blob: c43ad141d2bdffda9677b252da3903d2e0dc0ac0 [file]
// 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/offline_pages/recent_tab_suggestions_provider.h"
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/ntp_snippets/category.h"
#include "components/ntp_snippets/content_suggestions_provider.h"
#include "components/ntp_snippets/mock_content_suggestions_provider_observer.h"
#include "components/ntp_snippets/offline_pages/offline_pages_test_utils.h"
#include "components/offline_pages/core/background/request_coordinator_stub_taco.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/downloads/download_ui_adapter.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/recent_tabs/recent_tabs_ui_adapter_delegate.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ntp_snippets::test::CaptureDismissedSuggestions;
using ntp_snippets::test::FakeOfflinePageModel;
using offline_pages::ClientId;
using offline_pages::MultipleOfflinePageItemCallback;
using offline_pages::OfflinePageItem;
using testing::_;
using testing::IsEmpty;
using testing::Mock;
using testing::Property;
using testing::SizeIs;
namespace ntp_snippets {
namespace {
OfflinePageItem CreateDummyRecentTab(int offline_id) {
// This is used to assign unique tab IDs to pages. Since offline IDs are
// typically small integers like 1, 2, 3 etc, we start at 1001 to ensure that
// they are different, and can catch bugs where offline page ID is used in
// place of tab ID and vice versa.
std::string tab_id = base::IntToString(offline_id + 1000);
ClientId client_id(offline_pages::kLastNNamespace, tab_id);
return test::CreateDummyOfflinePageItem(offline_id, client_id);
}
std::vector<OfflinePageItem> CreateDummyRecentTabs(
const std::vector<int>& ids) {
std::vector<OfflinePageItem> result;
for (int id : ids) {
result.push_back(CreateDummyRecentTab(id));
}
return result;
}
OfflinePageItem CreateDummyRecentTab(int id, base::Time time) {
OfflinePageItem item = CreateDummyRecentTab(id);
item.creation_time = time;
item.last_access_time = time;
return item;
}
} // namespace
class RecentTabSuggestionsProviderTestNoLoad : public testing::Test {
public:
RecentTabSuggestionsProviderTestNoLoad()
: task_runner_(new base::TestSimpleTaskRunner()),
task_runner_handle_(task_runner_),
pref_service_(new TestingPrefServiceSimple()) {
RecentTabSuggestionsProvider::RegisterProfilePrefs(
pref_service()->registry());
taco_ = base::MakeUnique<offline_pages::RequestCoordinatorStubTaco>();
taco_->CreateRequestCoordinator();
ui_adapter_ = offline_pages::RecentTabsUIAdapterDelegate::
GetOrCreateRecentTabsUIAdapter(&model_, taco_->request_coordinator());
delegate_ =
offline_pages::RecentTabsUIAdapterDelegate::FromDownloadUIAdapter(
ui_adapter_);
provider_ = base::MakeUnique<RecentTabSuggestionsProvider>(
&observer_, ui_adapter_, pref_service());
}
Category recent_tabs_category() {
return Category::FromKnownCategory(KnownCategories::RECENT_TABS);
}
ContentSuggestion::ID GetDummySuggestionId(int id) {
return ContentSuggestion::ID(recent_tabs_category(), base::IntToString(id));
}
void AddTabAndOfflinePageToModel(const OfflinePageItem& item) {
AddTab(offline_pages::RecentTabsUIAdapterDelegate::TabIdFromClientId(
item.client_id));
AddOfflinePageToModel(item);
}
void AddTab(int tab_id) { delegate_->RegisterTab(tab_id); }
void RemoveTab(int tab_id) { delegate_->UnregisterTab(tab_id); }
void AddOfflinePageToModel(const OfflinePageItem& item) {
ui_adapter_->OfflinePageAdded(&model_, item);
}
void FireOfflinePageDeleted(const OfflinePageItem& item) {
int tab_id = offline_pages::RecentTabsUIAdapterDelegate::TabIdFromClientId(
item.client_id);
RemoveTab(tab_id);
ui_adapter_->OfflinePageDeleted(
offline_pages::OfflinePageModel::DeletedPageInfo(
item.offline_id, item.client_id, "" /* request_origin */));
}
std::set<std::string> ReadDismissedIDsFromPrefs() {
return provider_->ReadDismissedIDsFromPrefs();
}
RecentTabSuggestionsProvider* provider() { return provider_.get(); }
MockContentSuggestionsProviderObserver* observer() { return &observer_; }
TestingPrefServiceSimple* pref_service() { return pref_service_.get(); }
base::TestSimpleTaskRunner* task_runner() { return task_runner_.get(); }
private:
FakeOfflinePageModel model_;
offline_pages::DownloadUIAdapter* ui_adapter_;
offline_pages::RecentTabsUIAdapterDelegate* delegate_;
scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
base::ThreadTaskRunnerHandle task_runner_handle_;
std::unique_ptr<offline_pages::RequestCoordinatorStubTaco> taco_;
MockContentSuggestionsProviderObserver observer_;
std::unique_ptr<TestingPrefServiceSimple> pref_service_;
// Last so that the dependencies are deleted after the provider.
std::unique_ptr<RecentTabSuggestionsProvider> provider_;
DISALLOW_COPY_AND_ASSIGN(RecentTabSuggestionsProviderTestNoLoad);
};
// Test that always loads the model before the start of the test.
class RecentTabSuggestionsProviderTest
: public RecentTabSuggestionsProviderTestNoLoad {
public:
RecentTabSuggestionsProviderTest() = default;
void SetUp() override {
// The UI adapter always fires asynchronously upon loading, so we want to
// run past that moment before each test. Expect a call to hide warnings.
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(1);
task_runner()->RunUntilIdle();
Mock::VerifyAndClearExpectations(observer());
}
private:
DISALLOW_COPY_AND_ASSIGN(RecentTabSuggestionsProviderTest);
};
TEST_F(RecentTabSuggestionsProviderTest, ShouldConvertToSuggestions) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(2);
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/3")))));
auto recent_tabs_list = CreateDummyRecentTabs({1, 2, 3});
for (OfflinePageItem& recent_tab : recent_tabs_list) {
AddTabAndOfflinePageToModel(recent_tab);
}
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldSortByCreationTime) {
base::Time now = base::Time::Now();
base::Time yesterday = now - base::TimeDelta::FromDays(1);
base::Time tomorrow = now + base::TimeDelta::FromDays(1);
std::vector<OfflinePageItem> offline_pages = {
CreateDummyRecentTab(1, now), CreateDummyRecentTab(2, yesterday),
CreateDummyRecentTab(3, tomorrow)};
EXPECT_CALL(
*observer(),
OnNewSuggestions(_, recent_tabs_category(),
ElementsAre(Property(&ContentSuggestion::url,
GURL("http://dummy.com/1")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(1, now));
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
ElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(2, yesterday));
offline_pages[1].last_access_time =
offline_pages[0].last_access_time + base::TimeDelta::FromHours(1);
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
ElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/3")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(3, tomorrow));
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldDeliverCorrectCategoryInfo) {
EXPECT_EQ(
ContentSuggestionsAdditionalAction::NONE,
provider()->GetCategoryInfo(recent_tabs_category()).additional_action());
}
// TODO(vitaliii): Break this test into multiple tests. Currently if it fails,
// it takes long time to find which part of it actually fails.
TEST_F(RecentTabSuggestionsProviderTest, ShouldDismiss) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(3);
auto recent_tabs_list = CreateDummyRecentTabs({1, 2, 3});
for (OfflinePageItem& recent_tab : recent_tabs_list) {
AddTabAndOfflinePageToModel(recent_tab);
}
// Dismiss 2 and 3.
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
provider()->DismissSuggestion(GetDummySuggestionId(2));
provider()->DismissSuggestion(GetDummySuggestionId(3));
Mock::VerifyAndClearExpectations(observer());
// They should disappear from the reported suggestions.
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/4")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(4));
Mock::VerifyAndClearExpectations(observer());
// And appear in the dismissed suggestions.
std::vector<ContentSuggestion> dismissed_suggestions;
provider()->GetDismissedSuggestionsForDebugging(
recent_tabs_category(),
base::Bind(&CaptureDismissedSuggestions, &dismissed_suggestions));
EXPECT_THAT(
dismissed_suggestions,
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/3"))));
// Clear dismissed suggestions.
provider()->ClearDismissedSuggestionsForDebugging(recent_tabs_category());
// They should be gone from the dismissed suggestions.
dismissed_suggestions.clear();
provider()->GetDismissedSuggestionsForDebugging(
recent_tabs_category(),
base::Bind(&CaptureDismissedSuggestions, &dismissed_suggestions));
EXPECT_THAT(dismissed_suggestions, IsEmpty());
// And appear in the reported suggestions for the category again.
EXPECT_CALL(*observer(),
OnNewSuggestions(_, recent_tabs_category(), SizeIs(5)));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(5));
Mock::VerifyAndClearExpectations(observer());
}
TEST_F(RecentTabSuggestionsProviderTest,
ShouldInvalidateWhenOfflinePageDeleted) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(3);
std::vector<OfflinePageItem> offline_pages = CreateDummyRecentTabs({1, 2, 3});
for (OfflinePageItem& recent_tab : offline_pages)
AddTabAndOfflinePageToModel(recent_tab);
// Invalidation of suggestion 2 should be forwarded.
EXPECT_CALL(*observer(), OnSuggestionInvalidated(_, GetDummySuggestionId(2)));
FireOfflinePageDeleted(offline_pages[1]);
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldClearDismissedOnInvalidate) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(3);
std::vector<OfflinePageItem> offline_pages = CreateDummyRecentTabs({1, 2, 3});
for (OfflinePageItem& recent_tab : offline_pages)
AddTabAndOfflinePageToModel(recent_tab);
EXPECT_THAT(ReadDismissedIDsFromPrefs(), IsEmpty());
provider()->DismissSuggestion(GetDummySuggestionId(2));
EXPECT_THAT(ReadDismissedIDsFromPrefs(), SizeIs(1));
FireOfflinePageDeleted(offline_pages[1]);
EXPECT_THAT(ReadDismissedIDsFromPrefs(), IsEmpty());
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldClearDismissedOnFetch) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(3);
std::vector<OfflinePageItem> offline_pages = CreateDummyRecentTabs({1, 2, 3});
for (OfflinePageItem& recent_tab : offline_pages)
AddTabAndOfflinePageToModel(recent_tab);
provider()->DismissSuggestion(GetDummySuggestionId(2));
provider()->DismissSuggestion(GetDummySuggestionId(3));
EXPECT_THAT(ReadDismissedIDsFromPrefs(), SizeIs(2));
FireOfflinePageDeleted(offline_pages[0]);
FireOfflinePageDeleted(offline_pages[2]);
EXPECT_THAT(ReadDismissedIDsFromPrefs(), SizeIs(1));
FireOfflinePageDeleted(offline_pages[1]);
EXPECT_THAT(ReadDismissedIDsFromPrefs(), IsEmpty());
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldNotShowSameUrlMutlipleTimes) {
base::Time now = base::Time::Now();
base::Time yesterday = now - base::TimeDelta::FromDays(1);
base::Time tomorrow = now + base::TimeDelta::FromDays(1);
std::vector<OfflinePageItem> offline_pages = {
CreateDummyRecentTab(1, yesterday), CreateDummyRecentTab(2, now),
CreateDummyRecentTab(3, tomorrow)};
// We leave IDs different, but make the URLs the same.
offline_pages[2].url = offline_pages[0].url;
AddTabAndOfflinePageToModel(offline_pages[0]);
AddTabAndOfflinePageToModel(offline_pages[1]);
Mock::VerifyAndClearExpectations(observer());
EXPECT_CALL(*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::publish_date, now),
Property(&ContentSuggestion::publish_date, tomorrow))));
AddTabAndOfflinePageToModel(offline_pages[2]);
}
TEST_F(RecentTabSuggestionsProviderTest,
ShouldNotFetchIfAddedOfflinePageIsNotRecentTab) {
// It should not fetch when not a recent tab is added, thus, it should not
// report the first recent tab (which it is not aware about).
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
AddOfflinePageToModel(ntp_snippets::test::CreateDummyOfflinePageItem(
2, offline_pages::kDefaultNamespace));
}
TEST_F(RecentTabSuggestionsProviderTest,
ShouldInvalidateSuggestionWhenTabGone) {
OfflinePageItem first_tab = CreateDummyRecentTab(1);
AddTabAndOfflinePageToModel(first_tab);
Mock::VerifyAndClearExpectations(observer());
EXPECT_CALL(*observer(), OnSuggestionInvalidated(_, GetDummySuggestionId(1)))
.Times(1);
RemoveTab(offline_pages::RecentTabsUIAdapterDelegate::TabIdFromClientId(
first_tab.client_id));
// Removing an unknown tab should not cause extra invalidations.
RemoveTab(42);
}
TEST_F(RecentTabSuggestionsProviderTest, ShouldNotShowPagesWithoutTab) {
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
// The provider is not notified about the first recent tab yet (no tab).
OfflinePageItem first_tab = CreateDummyRecentTab(1);
AddOfflinePageToModel(first_tab);
Mock::VerifyAndClearExpectations(observer());
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(1);
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")))));
AddTab(offline_pages::RecentTabsUIAdapterDelegate::TabIdFromClientId(
first_tab.client_id));
OfflinePageItem second_tab = CreateDummyRecentTab(2);
AddTabAndOfflinePageToModel(second_tab);
Mock::VerifyAndClearExpectations(observer());
EXPECT_CALL(*observer(), OnNewSuggestions(_, _, _)).Times(0);
// |RemoveTab| by itself doesn't cause OnNewSuggestions to be called.
RemoveTab(offline_pages::RecentTabsUIAdapterDelegate::TabIdFromClientId(
second_tab.client_id));
Mock::VerifyAndClearExpectations(observer());
// But when we get another tab, OnNewSuggestions will be called.
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/3")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(3));
}
// The following test uses a different fixture that does not automatically pump
// the event loop in SetUp, which means that until |RunUntilIdle| is called, the
// UI adapter will not be loaded (and should not fire any events).
TEST_F(RecentTabSuggestionsProviderTestNoLoad, ShouldFetchOnLoad) {
// Tabs are added to the model before the UI adapter is loaded, so there
// should only be a single |OnNewSuggestions| call, at load time.
EXPECT_CALL(
*observer(),
OnNewSuggestions(
_, recent_tabs_category(),
UnorderedElementsAre(
Property(&ContentSuggestion::url, GURL("http://dummy.com/1")),
Property(&ContentSuggestion::url, GURL("http://dummy.com/2")))));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(1));
AddTabAndOfflinePageToModel(CreateDummyRecentTab(2));
// The provider is not notified about the recent tabs yet.
task_runner()->RunUntilIdle();
// However, it must return both tabs when the model is loaded.
}
} // namespace ntp_snippets