blob: 81947992c1161287972c6bcf3b4d6d566364b89e [file] [log] [blame]
// Copyright 2020 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/feed/core/v2/feed_stream.h"
#include <map>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/optional.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/task_environment.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "components/feed/core/common/pref_names.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/proto/v2/ui.pb.h"
#include "components/feed/core/proto/v2/wire/action_request.pb.h"
#include "components/feed/core/proto/v2/wire/request.pb.h"
#include "components/feed/core/proto/v2/wire/there_and_back_again_data.pb.h"
#include "components/feed/core/proto/v2/xsurface.pb.h"
#include "components/feed/core/shared_prefs/pref_names.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/feed_network.h"
#include "components/feed/core/v2/metrics_reporter.h"
#include "components/feed/core/v2/protocol_translator.h"
#include "components/feed/core/v2/refresh_task_scheduler.h"
#include "components/feed/core/v2/scheduling.h"
#include "components/feed/core/v2/stream_model.h"
#include "components/feed/core/v2/tasks/load_stream_from_store_task.h"
#include "components/feed/core/v2/test/callback_receiver.h"
#include "components/feed/core/v2/test/proto_printer.h"
#include "components/feed/core/v2/test/stream_builder.h"
#include "components/leveldb_proto/public/proto_database_provider.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/page_criteria.h"
#include "components/offline_pages/core/prefetch/stub_prefetch_service.h"
#include "components/offline_pages/core/stub_offline_page_model.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace feed {
namespace {
std::unique_ptr<StreamModel> LoadModelFromStore(FeedStore* store) {
LoadStreamFromStoreTask::Result result;
auto complete = [&](LoadStreamFromStoreTask::Result task_result) {
result = std::move(task_result);
};
LoadStreamFromStoreTask load_task(
LoadStreamFromStoreTask::LoadType::kFullLoad, store, /*clock=*/nullptr,
base::BindLambdaForTesting(complete));
// We want to load the data no matter how stale.
load_task.IgnoreStalenessForTesting();
base::RunLoop run_loop;
load_task.Execute(run_loop.QuitClosure());
run_loop.Run();
if (result.status == LoadStreamStatus::kLoadedFromStore) {
auto model = std::make_unique<StreamModel>();
model->Update(std::move(result.update_request));
return model;
}
LOG(WARNING) << "LoadModelFromStore failed with " << result.status;
return nullptr;
}
// Returns the model state string (|StreamModel::DumpStateForTesting()|),
// given a model initialized with |update_request| and having |operations|
// applied.
std::string ModelStateFor(
std::unique_ptr<StreamModelUpdateRequest> update_request,
std::vector<feedstore::DataOperation> operations = {},
std::vector<feedstore::DataOperation> more_operations = {}) {
StreamModel model;
model.Update(std::move(update_request));
model.ExecuteOperations(operations);
model.ExecuteOperations(more_operations);
return model.DumpStateForTesting();
}
// Returns the model state string (|StreamModel::DumpStateForTesting()|),
// given a model initialized with |store|.
std::string ModelStateFor(FeedStore* store) {
auto model = LoadModelFromStore(store);
if (model) {
return model->DumpStateForTesting();
}
return "{Failed to load model from store}";
}
feedwire::FeedAction MakeFeedAction(int64_t id, size_t pad_size = 0) {
feedwire::FeedAction action;
action.mutable_content_id()->set_id(id);
action.mutable_content_id()->set_content_domain(std::string(pad_size, 'a'));
return action;
}
std::vector<feedstore::StoredAction> ReadStoredActions(FeedStore* store) {
base::RunLoop run_loop;
CallbackReceiver<std::vector<feedstore::StoredAction>> cr(&run_loop);
store->ReadActions(cr.Bind());
run_loop.Run();
CHECK(cr.GetResult());
return std::move(*cr.GetResult());
}
std::string SerializedOfflineBadgeContent() {
feedxsurface::OfflineBadgeContent testbadge;
std::string badge_serialized;
testbadge.set_available_offline(true);
testbadge.SerializeToString(&badge_serialized);
return badge_serialized;
}
// This is EXPECT_EQ, but also dumps the string values for ease of reading.
#define EXPECT_STRINGS_EQUAL(WANT, GOT) \
{ \
std::string want = (WANT), got = (GOT); \
EXPECT_EQ(want, got) << "Wanted:\n" << (want) << "\nBut got:\n" << (got); \
}
class TestSurface : public FeedStream::SurfaceInterface {
public:
// Provide some helper functionality to attach/detach the surface.
// This way we can auto-detach in the destructor.
explicit TestSurface(FeedStream* stream = nullptr) {
if (stream)
Attach(stream);
}
~TestSurface() override {
if (stream_)
Detach();
}
void Attach(FeedStream* stream) {
EXPECT_FALSE(stream_);
stream_ = stream;
stream_->AttachSurface(this);
}
void Detach() {
EXPECT_TRUE(stream_);
stream_->DetachSurface(this);
stream_ = nullptr;
}
// FeedStream::SurfaceInterface.
void StreamUpdate(const feedui::StreamUpdate& stream_update) override {
DVLOG(1) << "StreamUpdate: " << stream_update;
// Some special-case treatment for the loading spinner. We don't count it
// toward |initial_state|.
bool is_initial_loading_spinner = IsInitialLoadSpinnerUpdate(stream_update);
if (!initial_state && !is_initial_loading_spinner) {
initial_state = stream_update;
}
update = stream_update;
described_updates_.push_back(CurrentState());
}
void ReplaceDataStoreEntry(base::StringPiece key,
base::StringPiece data) override {
data_store_entries_[key.as_string()] = data.as_string();
}
void RemoveDataStoreEntry(base::StringPiece key) override {
data_store_entries_.erase(key.as_string());
}
// Test functions.
void Clear() {
initial_state = base::nullopt;
update = base::nullopt;
described_updates_.clear();
}
// Returns a description of the updates this surface received. Each update
// is separated by ' -> '. Returns only the updates since the last call.
std::string DescribeUpdates() {
std::string result = base::JoinString(described_updates_, " -> ");
described_updates_.clear();
return result;
}
std::map<std::string, std::string> GetDataStoreEntries() const {
return data_store_entries_;
}
// The initial state of the stream, if it was received. This is nullopt if
// only the loading spinner was seen.
base::Optional<feedui::StreamUpdate> initial_state;
// The last stream update received.
base::Optional<feedui::StreamUpdate> update;
private:
std::string CurrentState() {
if (update && IsInitialLoadSpinnerUpdate(*update))
return "loading";
if (!initial_state)
return "empty";
bool has_loading_spinner = false;
for (int i = 0; i < update->updated_slices().size(); ++i) {
const feedui::StreamUpdate_SliceUpdate& slice_update =
update->updated_slices(i);
if (slice_update.has_slice() &&
slice_update.slice().has_zero_state_slice()) {
CHECK(update->updated_slices().size() == 1)
<< "Zero state with other slices" << *update;
// Returns either "no-cards" or "cant-refresh".
return update->updated_slices()[0].slice().slice_id();
}
if (slice_update.has_slice() &&
slice_update.slice().has_loading_spinner_slice()) {
CHECK_EQ(i, update->updated_slices().size() - 1)
<< "Loading spinner in an unexpected place" << *update;
has_loading_spinner = true;
}
}
std::stringstream ss;
if (has_loading_spinner) {
ss << update->updated_slices().size() - 1 << " slices +spinner";
} else {
ss << update->updated_slices().size() << " slices";
}
return ss.str();
}
bool IsInitialLoadSpinnerUpdate(const feedui::StreamUpdate& update) {
return update.updated_slices().size() == 1 &&
update.updated_slices()[0].has_slice() &&
update.updated_slices()[0].slice().has_loading_spinner_slice();
}
// The stream if it was attached using the constructor.
FeedStream* stream_ = nullptr;
std::vector<std::string> described_updates_;
std::map<std::string, std::string> data_store_entries_;
};
class TestFeedNetwork : public FeedNetwork {
public:
// FeedNetwork implementation.
void SendQueryRequest(
const feedwire::Request& request,
base::OnceCallback<void(QueryRequestResult)> callback) override {
++send_query_call_count;
// Emulate a successful response.
// The response body is currently an empty message, because most of the
// time we want to inject a translated response for ease of test-writing.
query_request_sent = request;
QueryRequestResult result;
result.response_info.status_code = 200;
result.response_info.response_body_bytes = 100;
result.response_info.fetch_duration = base::TimeDelta::FromMilliseconds(42);
if (injected_response_) {
result.response_body = std::make_unique<feedwire::Response>(
std::move(injected_response_.value()));
} else {
result.response_body = std::make_unique<feedwire::Response>();
}
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
}
void SendActionRequest(
const feedwire::ActionRequest& request,
base::OnceCallback<void(ActionRequestResult)> callback) override {
action_request_sent = request;
++action_request_call_count;
ActionRequestResult result;
if (injected_action_result != base::nullopt) {
result = std::move(*injected_action_result);
} else {
auto response = std::make_unique<feedwire::Response>();
response->mutable_feed_response()
->mutable_feed_response()
->mutable_consistency_token()
->set_token(consistency_token);
result.response_body = std::move(response);
}
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
}
void CancelRequests() override { NOTIMPLEMENTED(); }
void InjectRealResponse() {
base::FilePath response_file_path;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &response_file_path));
response_file_path = response_file_path.AppendASCII(
"components/test/data/feed/response.binarypb");
std::string response_data;
CHECK(base::ReadFileToString(response_file_path, &response_data));
feedwire::Response response;
CHECK(response.ParseFromString(response_data));
injected_response_ = response;
}
base::Optional<feedwire::Request> query_request_sent;
int send_query_call_count = 0;
void InjectActionRequestResult(ActionRequestResult result) {
injected_action_result = std::move(result);
}
void InjectEmptyActionRequestResult() {
ActionRequestResult result;
result.response_body = nullptr;
InjectActionRequestResult(std::move(result));
}
base::Optional<feedwire::ActionRequest> action_request_sent;
int action_request_call_count = 0;
std::string consistency_token;
private:
base::Optional<feedwire::Response> injected_response_;
base::Optional<ActionRequestResult> injected_action_result;
};
// Forwards to |FeedStream::WireResponseTranslator| unless a response is
// injected.
class TestWireResponseTranslator : public FeedStream::WireResponseTranslator {
public:
RefreshResponseData TranslateWireResponse(
feedwire::Response response,
StreamModelUpdateRequest::Source source,
base::Time current_time) override {
if (injected_response_) {
if (injected_response_->model_update_request)
injected_response_->model_update_request->source = source;
RefreshResponseData result = std::move(*injected_response_);
injected_response_.reset();
return result;
}
return FeedStream::WireResponseTranslator::TranslateWireResponse(
std::move(response), source, current_time);
}
void InjectResponse(std::unique_ptr<StreamModelUpdateRequest> response) {
injected_response_ = RefreshResponseData();
injected_response_->model_update_request = std::move(response);
}
void InjectResponse(RefreshResponseData response_data) {
injected_response_ = std::move(response_data);
}
bool InjectedResponseConsumed() const { return !injected_response_; }
private:
base::Optional<RefreshResponseData> injected_response_;
};
class FakeRefreshTaskScheduler : public RefreshTaskScheduler {
public:
// RefreshTaskScheduler implementation.
void EnsureScheduled(base::TimeDelta run_time) override {
scheduled_run_time = run_time;
}
void Cancel() override { canceled = true; }
void RefreshTaskComplete() override { refresh_task_complete = true; }
void Clear() {
scheduled_run_time.reset();
canceled = false;
refresh_task_complete = false;
}
base::Optional<base::TimeDelta> scheduled_run_time;
bool canceled = false;
bool refresh_task_complete = false;
};
class TestMetricsReporter : public MetricsReporter {
public:
explicit TestMetricsReporter(const base::TickClock* clock, PrefService* prefs)
: MetricsReporter(clock, prefs) {}
// MetricsReporter.
void ContentSliceViewed(SurfaceId surface_id, int index_in_stream) override {
slice_viewed_index = index_in_stream;
MetricsReporter::ContentSliceViewed(surface_id, index_in_stream);
}
void OnLoadStream(LoadStreamStatus load_from_store_status,
LoadStreamStatus final_status) override {
load_stream_status = final_status;
LOG(INFO) << "OnLoadStream: " << final_status
<< " (store status: " << load_from_store_status << ")";
MetricsReporter::OnLoadStream(load_from_store_status, final_status);
}
void OnLoadMore(LoadStreamStatus final_status) override {
load_more_status = final_status;
MetricsReporter::OnLoadMore(final_status);
}
void OnBackgroundRefresh(LoadStreamStatus final_status) override {
background_refresh_status = final_status;
MetricsReporter::OnBackgroundRefresh(final_status);
}
void OnClearAll(base::TimeDelta time_since_last_clear) override {
this->time_since_last_clear = time_since_last_clear;
MetricsReporter::OnClearAll(time_since_last_clear);
}
// Test access.
base::Optional<int> slice_viewed_index;
base::Optional<LoadStreamStatus> load_stream_status;
base::Optional<LoadStreamStatus> load_more_status;
base::Optional<LoadStreamStatus> background_refresh_status;
base::Optional<base::TimeDelta> time_since_last_clear;
base::Optional<TriggerType> refresh_trigger_type;
};
class TestPrefetchService : public offline_pages::StubPrefetchService {
public:
TestPrefetchService() = default;
// offline_pages::StubPrefetchService.
void SetSuggestionProvider(
offline_pages::SuggestionsProvider* suggestions_provider) override {
suggestions_provider_ = suggestions_provider;
}
void NewSuggestionsAvailable() override {
++new_suggestions_available_call_count_;
}
// Test functionality.
offline_pages::SuggestionsProvider* suggestions_provider() {
return suggestions_provider_;
}
int NewSuggestionsAvailableCallCount() const {
return new_suggestions_available_call_count_;
}
private:
offline_pages::SuggestionsProvider* suggestions_provider_ = nullptr;
int new_suggestions_available_call_count_ = 0;
};
class TestOfflinePageModel : public offline_pages::StubOfflinePageModel {
public:
// offline_pages::OfflinePageModel
void AddObserver(Observer* observer) override {
CHECK(observers_.insert(observer).second);
}
void RemoveObserver(Observer* observer) override {
CHECK_EQ(1UL, observers_.erase(observer));
}
void GetPagesWithCriteria(
const offline_pages::PageCriteria& criteria,
offline_pages::MultipleOfflinePageItemCallback callback) override {
std::vector<offline_pages::OfflinePageItem> result;
for (const offline_pages::OfflinePageItem& item : items_) {
if (MeetsCriteria(criteria, item)) {
result.push_back(item);
}
}
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), result));
}
// Test functions.
void AddTestPage(const GURL& url) {
offline_pages::OfflinePageItem item;
item.url = url;
item.client_id =
offline_pages::ClientId(offline_pages::kSuggestedArticlesNamespace, "");
items_.push_back(item);
}
std::vector<offline_pages::OfflinePageItem>& items() { return items_; }
void CallObserverOfflinePageAdded(
const offline_pages::OfflinePageItem& item) {
for (Observer* observer : observers_) {
observer->OfflinePageAdded(this, item);
}
}
void CallObserverOfflinePageDeleted(
const offline_pages::OfflinePageItem& item) {
for (Observer* observer : observers_) {
observer->OfflinePageDeleted(item);
}
}
private:
std::vector<offline_pages::OfflinePageItem> items_;
std::set<Observer*> observers_;
};
class FeedStreamTest : public testing::Test, public FeedStream::Delegate {
public:
void SetUp() override {
// Reset to default config, since tests can change it.
SetFeedConfigForTesting(Config());
feed::prefs::RegisterFeedSharedProfilePrefs(profile_prefs_.registry());
feed::RegisterProfilePrefs(profile_prefs_.registry());
metrics_reporter_ = std::make_unique<TestMetricsReporter>(
task_environment_.GetMockTickClock(), &profile_prefs_);
CHECK_EQ(kTestTimeEpoch, task_environment_.GetMockClock()->Now());
CreateStream();
}
void TearDown() override {
// Ensure the task queue can return to idle. Failure to do so may be due
// to a stuck task that never called |TaskComplete()|.
WaitForIdleTaskQueue();
// Store requires PostTask to clean up.
store_.reset();
task_environment_.RunUntilIdle();
}
// FeedStream::Delegate.
bool IsEulaAccepted() override { return is_eula_accepted_; }
bool IsOffline() override { return is_offline_; }
DisplayMetrics GetDisplayMetrics() override {
DisplayMetrics result;
result.density = 200;
result.height_pixels = 800;
result.width_pixels = 350;
return result;
}
std::string GetLanguageTag() override { return "en-US"; }
// For tests.
// Replace stream_.
void CreateStream() {
ChromeInfo chrome_info;
chrome_info.channel = version_info::Channel::STABLE;
chrome_info.version = base::Version({99, 1, 9911, 2});
stream_ = std::make_unique<FeedStream>(
&refresh_scheduler_, metrics_reporter_.get(), this, &profile_prefs_,
&network_, store_.get(), &prefetch_service_, &offline_page_model_,
task_environment_.GetMockClock(), task_environment_.GetMockTickClock(),
chrome_info);
WaitForIdleTaskQueue(); // Wait for any initialization.
stream_->SetWireResponseTranslatorForTesting(&response_translator_);
}
bool IsTaskQueueIdle() const {
return !stream_->GetTaskQueueForTesting()->HasPendingTasks() &&
!stream_->GetTaskQueueForTesting()->HasRunningTask();
}
void WaitForIdleTaskQueue() {
if (IsTaskQueueIdle())
return;
base::test::ScopedRunLoopTimeout run_timeout(
FROM_HERE, base::TimeDelta::FromSeconds(1));
base::RunLoop run_loop;
stream_->SetIdleCallbackForTesting(run_loop.QuitClosure());
run_loop.Run();
}
void UnloadModel() {
WaitForIdleTaskQueue();
stream_->UnloadModel();
}
// Dumps the state of |FeedStore| to a string for debugging.
std::string DumpStoreState() {
base::RunLoop run_loop;
std::unique_ptr<std::vector<feedstore::Record>> records;
auto callback =
[&](bool, std::unique_ptr<std::vector<feedstore::Record>> result) {
records = std::move(result);
run_loop.Quit();
};
store_->GetDatabaseForTesting()->LoadEntries(
base::BindLambdaForTesting(callback));
run_loop.Run();
std::stringstream ss;
for (const feedstore::Record& record : *records) {
ss << record << '\n';
}
return ss.str();
}
void UploadActions(std::vector<feedwire::FeedAction> actions) {
size_t actions_remaining = actions.size();
for (feedwire::FeedAction& action : actions) {
stream_->UploadAction(action, (--actions_remaining) == 0ul,
base::DoNothing());
}
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
TestingPrefServiceSimple profile_prefs_;
std::unique_ptr<TestMetricsReporter> metrics_reporter_;
TestFeedNetwork network_;
TestWireResponseTranslator response_translator_;
std::unique_ptr<FeedStore> store_ = std::make_unique<FeedStore>(
leveldb_proto::ProtoDatabaseProvider::GetUniqueDB<feedstore::Record>(
leveldb_proto::ProtoDbType::FEED_STREAM_DATABASE,
/*file_path=*/{},
task_environment_.GetMainThreadTaskRunner()));
FakeRefreshTaskScheduler refresh_scheduler_;
TestPrefetchService prefetch_service_;
TestOfflinePageModel offline_page_model_;
std::unique_ptr<FeedStream> stream_;
bool is_eula_accepted_ = true;
bool is_offline_ = false;
};
TEST_F(FeedStreamTest, IsArticlesListVisibleByDefault) {
EXPECT_TRUE(stream_->IsArticlesListVisible());
}
TEST_F(FeedStreamTest, SetArticlesListVisible) {
EXPECT_TRUE(stream_->IsArticlesListVisible());
stream_->SetArticlesListVisible(false);
EXPECT_FALSE(stream_->IsArticlesListVisible());
stream_->SetArticlesListVisible(true);
EXPECT_TRUE(stream_->IsArticlesListVisible());
}
TEST_F(FeedStreamTest, DoNotRefreshIfArticlesListIsHidden) {
stream_->SetArticlesListVisible(false);
stream_->InitializeScheduling();
EXPECT_TRUE(refresh_scheduler_.canceled);
stream_->ExecuteRefreshTask();
EXPECT_TRUE(refresh_scheduler_.refresh_task_complete);
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden,
metrics_reporter_->background_refresh_status);
}
TEST_F(FeedStreamTest, BackgroundRefreshSuccess) {
// Trigger a background refresh.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask();
WaitForIdleTaskQueue();
// Verify the refresh happened and that we can load a stream without the
// network.
ASSERT_TRUE(refresh_scheduler_.refresh_task_complete);
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->background_refresh_status);
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
EXPECT_FALSE(stream_->GetModel());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Verify that prefetch service was informed.
EXPECT_EQ(1, prefetch_service_.NewSuggestionsAvailableCallCount());
}
TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedWhenModelIsLoading) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
stream_->ExecuteRefreshTask();
WaitForIdleTaskQueue();
EXPECT_EQ(metrics_reporter_->background_refresh_status,
LoadStreamStatus::kModelAlreadyLoaded);
}
TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedAfterModelIsLoaded) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ExecuteRefreshTask();
WaitForIdleTaskQueue();
EXPECT_EQ(metrics_reporter_->background_refresh_status,
LoadStreamStatus::kModelAlreadyLoaded);
}
TEST_F(FeedStreamTest, SurfaceReceivesInitialContent) {
{
auto model = std::make_unique<StreamModel>();
model->Update(MakeTypicalInitialModelState());
stream_->LoadModelForTesting(std::move(model));
}
TestSurface surface(stream_.get());
ASSERT_TRUE(surface.initial_state);
const feedui::StreamUpdate& initial_state = surface.initial_state.value();
ASSERT_EQ(2, initial_state.updated_slices().size());
EXPECT_NE("", initial_state.updated_slices(0).slice().slice_id());
EXPECT_EQ("f:0", initial_state.updated_slices(0)
.slice()
.xsurface_slice()
.xsurface_frame());
EXPECT_NE("", initial_state.updated_slices(1).slice().slice_id());
EXPECT_EQ("f:1", initial_state.updated_slices(1)
.slice()
.xsurface_slice()
.xsurface_frame());
ASSERT_EQ(1, initial_state.new_shared_states().size());
EXPECT_EQ("ss:0",
initial_state.new_shared_states()[0].xsurface_shared_state());
}
TEST_F(FeedStreamTest, SurfaceReceivesInitialContentLoadedAfterAttach) {
TestSurface surface(stream_.get());
ASSERT_FALSE(surface.initial_state);
{
auto model = std::make_unique<StreamModel>();
model->Update(MakeTypicalInitialModelState());
stream_->LoadModelForTesting(std::move(model));
}
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
const feedui::StreamUpdate& initial_state = surface.initial_state.value();
EXPECT_NE("", initial_state.updated_slices(0).slice().slice_id());
EXPECT_EQ("f:0", initial_state.updated_slices(0)
.slice()
.xsurface_slice()
.xsurface_frame());
EXPECT_NE("", initial_state.updated_slices(1).slice().slice_id());
EXPECT_EQ("f:1", initial_state.updated_slices(1)
.slice()
.xsurface_slice()
.xsurface_frame());
ASSERT_EQ(1, initial_state.new_shared_states().size());
EXPECT_EQ("ss:0",
initial_state.new_shared_states()[0].xsurface_shared_state());
}
TEST_F(FeedStreamTest, SurfaceReceivesUpdatedContent) {
{
auto model = std::make_unique<StreamModel>();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(std::move(model));
}
TestSurface surface(stream_.get());
// Remove #1, add #2.
stream_->ExecuteOperations({
MakeOperation(MakeRemove(MakeClusterId(1))),
MakeOperation(MakeCluster(2, MakeRootId())),
MakeOperation(MakeContentNode(2, MakeClusterId(2))),
MakeOperation(MakeContent(2)),
});
ASSERT_TRUE(surface.update);
const feedui::StreamUpdate& initial_state = surface.initial_state.value();
const feedui::StreamUpdate& update = surface.update.value();
ASSERT_EQ("2 slices -> 2 slices", surface.DescribeUpdates());
// First slice is just an ID that matches the old 1st slice ID.
EXPECT_EQ(initial_state.updated_slices(0).slice().slice_id(),
update.updated_slices(0).slice_id());
// Second slice is a new xsurface slice.
EXPECT_NE("", update.updated_slices(1).slice().slice_id());
EXPECT_EQ("f:2",
update.updated_slices(1).slice().xsurface_slice().xsurface_frame());
}
TEST_F(FeedStreamTest, SurfaceReceivesSecondUpdatedContent) {
{
auto model = std::make_unique<StreamModel>();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(std::move(model));
}
TestSurface surface(stream_.get());
// Add #2.
stream_->ExecuteOperations({
MakeOperation(MakeCluster(2, MakeRootId())),
MakeOperation(MakeContentNode(2, MakeClusterId(2))),
MakeOperation(MakeContent(2)),
});
// Clear the last update and add #3.
stream_->ExecuteOperations({
MakeOperation(MakeCluster(3, MakeRootId())),
MakeOperation(MakeContentNode(3, MakeClusterId(3))),
MakeOperation(MakeContent(3)),
});
// The last update should have only one new piece of content.
// This verifies the current content set is tracked properly.
ASSERT_EQ("2 slices -> 3 slices -> 4 slices", surface.DescribeUpdates());
ASSERT_EQ(4, surface.update->updated_slices().size());
EXPECT_FALSE(surface.update->updated_slices(0).has_slice());
EXPECT_FALSE(surface.update->updated_slices(1).has_slice());
EXPECT_FALSE(surface.update->updated_slices(2).has_slice());
EXPECT_EQ("f:3", surface.update->updated_slices(3)
.slice()
.xsurface_slice()
.xsurface_frame());
}
TEST_F(FeedStreamTest, RemoveAllContentResultsInZeroState) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Remove both pieces of content.
stream_->ExecuteOperations({
MakeOperation(MakeRemove(MakeClusterId(0))),
MakeOperation(MakeRemove(MakeClusterId(1))),
});
ASSERT_EQ("loading -> 2 slices -> no-cards", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, DetachSurface) {
{
auto model = std::make_unique<StreamModel>();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(std::move(model));
}
TestSurface surface(stream_.get());
EXPECT_TRUE(surface.initial_state);
surface.Detach();
surface.Clear();
// Arbitrary stream change. Surface should not see the update.
stream_->ExecuteOperations({
MakeOperation(MakeRemove(MakeClusterId(1))),
});
EXPECT_FALSE(surface.update);
}
TEST_F(FeedStreamTest, LoadFromNetwork) {
stream_->GetMetadata()->SetConsistencyToken("token");
// Store is empty, so we should fallback to a network request.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_EQ(
"token",
network_.query_request_sent->feed_request().consistency_token().token());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Verify the model is filled correctly.
EXPECT_STRINGS_EQUAL(ModelStateFor(MakeTypicalInitialModelState()),
stream_->GetModel()->DumpStateForTesting());
// Verify the data was written to the store.
EXPECT_STRINGS_EQUAL(ModelStateFor(MakeTypicalInitialModelState()),
ModelStateFor(store_.get()));
}
TEST_F(FeedStreamTest, ForceRefreshForDebugging) {
// First do a normal load via network that will fail.
is_offline_ = true;
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Next, force a refresh that results in a successful load.
is_offline_ = false;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ForceRefreshForDebugging();
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> cant-refresh -> loading -> 2 slices",
surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, RefreshScheduleFlow) {
// Inject a typical network response, with a server-defined request schedule.
{
RequestSchedule schedule;
schedule.anchor_time = kTestTimeEpoch;
schedule.refresh_offsets = {base::TimeDelta::FromSeconds(12),
base::TimeDelta::FromSeconds(48)};
RefreshResponseData response_data;
response_data.model_update_request = MakeTypicalInitialModelState();
response_data.request_schedule = schedule;
response_translator_.InjectResponse(std::move(response_data));
}
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Verify the first refresh was scheduled.
EXPECT_EQ(base::TimeDelta::FromSeconds(12),
refresh_scheduler_.scheduled_run_time);
// Simulate executing the background task.
refresh_scheduler_.Clear();
task_environment_.AdvanceClock(base::TimeDelta::FromSeconds(12));
stream_->ExecuteRefreshTask();
WaitForIdleTaskQueue();
// Verify |RefreshTaskComplete()| was called and next refresh was scheduled.
EXPECT_TRUE(refresh_scheduler_.refresh_task_complete);
EXPECT_EQ(base::TimeDelta::FromSeconds(48 - 12),
refresh_scheduler_.scheduled_run_time);
// Simulate executing the background task again.
refresh_scheduler_.Clear();
task_environment_.AdvanceClock(base::TimeDelta::FromSeconds(48 - 12));
stream_->ExecuteRefreshTask();
WaitForIdleTaskQueue();
// Verify |RefreshTaskComplete()| was called and next refresh was scheduled.
EXPECT_TRUE(refresh_scheduler_.refresh_task_complete);
ASSERT_TRUE(refresh_scheduler_.scheduled_run_time);
EXPECT_EQ(GetFeedConfig().default_background_refresh_interval,
*refresh_scheduler_.scheduled_run_time);
}
TEST_F(FeedStreamTest, LoadFromNetworkBecauseStoreIsStale) {
// Fill the store with stream data that is just barely stale, and verify we
// fetch new data over the network.
store_->OverwriteStream(
MakeTypicalInitialModelState(
/*first_cluster_id=*/0, kTestTimeEpoch -
GetFeedConfig().stale_content_threshold -
base::TimeDelta::FromMinutes(1)),
base::DoNothing());
stream_->GetMetadata()->SetConsistencyToken("token-1");
// Store is stale, so we should fallback to a network request.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
// The stored continutation token should be sent.
EXPECT_EQ(
"token-1",
network_.query_request_sent->feed_request().consistency_token().token());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_TRUE(surface.initial_state);
}
TEST_F(FeedStreamTest, LoadFromNetworkFailsDueToProtoTranslation) {
// No data in the store, so we should fetch from the network.
// The network will respond with an empty response, which should fail proto
// translation.
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed,
metrics_reporter_->load_stream_status);
EXPECT_EQ(0, prefetch_service_.NewSuggestionsAvailableCallCount());
}
TEST_F(FeedStreamTest, DoNotLoadFromNetworkWhenOffline) {
is_offline_ = true;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkOffline,
metrics_reporter_->load_stream_status);
EXPECT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, DoNotLoadStreamWhenArticleListIsHidden) {
stream_->SetArticlesListVisible(false);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden,
metrics_reporter_->load_stream_status);
EXPECT_EQ("no-cards", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, DoNotLoadStreamWhenEulaIsNotAccepted) {
is_eula_accepted_ = false;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedEulaNotAccepted,
metrics_reporter_->load_stream_status);
EXPECT_EQ("no-cards", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, LoadStreamAfterEulaIsAccepted) {
// Connect a surface before the EULA is accepted.
is_eula_accepted_ = false;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("no-cards", surface.DescribeUpdates());
// Accept EULA, our surface should receive data.
is_eula_accepted_ = true;
stream_->OnEulaAccepted();
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, DoNotLoadFromNetworkAfterHistoryIsDeleted) {
stream_->OnHistoryDeleted();
task_environment_.FastForwardBy(kSuppressRefreshDuration -
base::TimeDelta::FromSeconds(1));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> no-cards", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete,
metrics_reporter_->load_stream_status);
surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2));
surface.Clear();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) {
LoadStreamStatus status = LoadStreamStatus::kNoStatus;
for (; status == LoadStreamStatus::kNoStatus;
status = stream_->ShouldMakeFeedQueryRequest()) {
}
ASSERT_EQ(LoadStreamStatus::kCannotLoadFromNetworkThrottled, status);
}
TEST_F(FeedStreamTest, LoadStreamFromStore) {
// Fill the store with stream data that is just barely fresh, and verify it
// loads.
store_->OverwriteStream(MakeTypicalInitialModelState(
/*first_cluster_id=*/0,
kTestTimeEpoch - base::TimeDelta::FromHours(12) +
base::TimeDelta::FromMinutes(1)),
base::DoNothing());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
EXPECT_FALSE(network_.query_request_sent);
// Verify the model is filled correctly.
EXPECT_STRINGS_EQUAL(ModelStateFor(MakeTypicalInitialModelState()),
stream_->GetModel()->DumpStateForTesting());
}
TEST_F(FeedStreamTest, LoadingSpinnerIsSentInitially) {
store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing());
TestSurface surface(stream_.get());
ASSERT_EQ("loading", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, DetachSurfaceWhileLoadingModel) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
surface.Detach();
WaitForIdleTaskQueue();
EXPECT_EQ("loading", surface.DescribeUpdates());
EXPECT_TRUE(network_.query_request_sent);
}
TEST_F(FeedStreamTest, AttachMultipleSurfacesLoadsModelOnce) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
TestSurface other_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
ASSERT_EQ("loading -> 2 slices", other_surface.DescribeUpdates());
// After load, another surface doesn't trigger any tasks,
// and immediately has content.
TestSurface later_surface(stream_.get());
ASSERT_EQ("2 slices", later_surface.DescribeUpdates());
EXPECT_TRUE(IsTaskQueueIdle());
}
TEST_F(FeedStreamTest, ModelChangesAreSavedToStorage) {
store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(surface.initial_state);
// Remove #1, add #2.
const std::vector<feedstore::DataOperation> operations = {
MakeOperation(MakeRemove(MakeClusterId(1))),
MakeOperation(MakeCluster(2, MakeRootId())),
MakeOperation(MakeContentNode(2, MakeClusterId(2))),
MakeOperation(MakeContent(2)),
};
stream_->ExecuteOperations(operations);
WaitForIdleTaskQueue();
// Verify changes are applied to storage.
EXPECT_STRINGS_EQUAL(
ModelStateFor(MakeTypicalInitialModelState(), operations),
ModelStateFor(store_.get()));
// Unload and reload the model from the store, and verify we can still apply
// operations correctly.
surface.Detach();
surface.Clear();
UnloadModel();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(surface.initial_state);
// Remove #2, add #3.
const std::vector<feedstore::DataOperation> operations2 = {
MakeOperation(MakeRemove(MakeClusterId(2))),
MakeOperation(MakeCluster(3, MakeRootId())),
MakeOperation(MakeContentNode(3, MakeClusterId(3))),
MakeOperation(MakeContent(3)),
};
stream_->ExecuteOperations(operations2);
WaitForIdleTaskQueue();
EXPECT_STRINGS_EQUAL(
ModelStateFor(MakeTypicalInitialModelState(), operations, operations2),
ModelStateFor(store_.get()));
}
TEST_F(FeedStreamTest, ReportSliceViewedIdentifiesCorrectIndex) {
store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing());
TestSurface surface;
stream_->AttachSurface(&surface);
WaitForIdleTaskQueue();
stream_->ReportSliceViewed(
surface.GetSurfaceId(),
surface.initial_state->updated_slices(1).slice().slice_id());
EXPECT_EQ(1, metrics_reporter_->slice_viewed_index);
}
TEST_F(FeedStreamTest, LoadMoreAppendsContent) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
EXPECT_EQ(1, prefetch_service_.NewSuggestionsAvailableCallCount());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
EXPECT_EQ("2 slices +spinner -> 4 slices", surface.DescribeUpdates());
EXPECT_EQ(2, prefetch_service_.NewSuggestionsAvailableCallCount());
// Load page 3.
response_translator_.InjectResponse(MakeTypicalNextPageState(3));
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
EXPECT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
EXPECT_EQ(3, prefetch_service_.NewSuggestionsAvailableCallCount());
}
TEST_F(FeedStreamTest, LoadMorePersistsData) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(stream_->GetModel()->DumpStateForTesting(),
ModelStateFor(store_.get()));
}
TEST_F(FeedStreamTest, LoadMorePersistAndLoadMore) {
// Verify we can persist a LoadMore, and then do another LoadMore after
// reloading state.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
surface.Detach();
UnloadModel();
// Load page 3.
surface.Attach(stream_.get());
response_translator_.InjectResponse(MakeTypicalNextPageState(3));
WaitForIdleTaskQueue();
callback.Clear();
surface.Clear();
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
ASSERT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(stream_->GetModel()->DumpStateForTesting(),
ModelStateFor(store_.get()));
}
TEST_F(FeedStreamTest, LoadMoreSendsTokens) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
stream_->GetMetadata()->SetConsistencyToken("token-1");
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ("2 slices +spinner -> 4 slices", surface.DescribeUpdates());
EXPECT_EQ(
"token-1",
network_.query_request_sent->feed_request().consistency_token().token());
EXPECT_EQ("page-2", network_.query_request_sent->feed_request()
.feed_query()
.next_page_token()
.next_page_token()
.next_page_token());
stream_->GetMetadata()->SetConsistencyToken("token-2");
response_translator_.InjectResponse(MakeTypicalNextPageState(3));
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
EXPECT_EQ(
"token-2",
network_.query_request_sent->feed_request().consistency_token().token());
EXPECT_EQ("page-3", network_.query_request_sent->feed_request()
.feed_query()
.next_page_token()
.next_page_token()
.next_page_token());
}
TEST_F(FeedStreamTest, LoadMoreAbortsIfNoNextPageToken) {
{
std::unique_ptr<StreamModelUpdateRequest> initial_state =
MakeTypicalInitialModelState();
initial_state->stream_data.clear_next_page_token();
response_translator_.InjectResponse(std::move(initial_state));
}
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
// LoadMore fails, and does not make an additional request.
EXPECT_EQ(base::Optional<bool>(false), callback.GetResult());
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, LoadMoreFail) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Don't inject another response, which results in a proto translation
// failure.
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(base::Optional<bool>(false), callback.GetResult());
EXPECT_EQ("2 slices +spinner -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, LoadMoreWithClearAllInResponse) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Use a different initial state (which includes a CLEAR_ALL).
response_translator_.InjectResponse(MakeTypicalInitialModelState(5));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface.GetSurfaceId(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(base::Optional<bool>(true), callback.GetResult());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(stream_->GetModel()->DumpStateForTesting(),
ModelStateFor(store_.get()));
// Verify the new state has been pushed to |surface|.
ASSERT_EQ("2 slices +spinner -> 2 slices", surface.DescribeUpdates());
const feedui::StreamUpdate& initial_state = surface.update.value();
ASSERT_EQ(2, initial_state.updated_slices().size());
EXPECT_NE("", initial_state.updated_slices(0).slice().slice_id());
EXPECT_EQ("f:5", initial_state.updated_slices(0)
.slice()
.xsurface_slice()
.xsurface_frame());
EXPECT_NE("", initial_state.updated_slices(1).slice().slice_id());
EXPECT_EQ("f:6", initial_state.updated_slices(1)
.slice()
.xsurface_slice()
.xsurface_frame());
}
TEST_F(FeedStreamTest, LoadMoreBeforeLoad) {
CallbackReceiver<bool> callback;
stream_->LoadMore(SurfaceId(), callback.Bind());
EXPECT_EQ(base::Optional<bool>(false), callback.GetResult());
}
TEST_F(FeedStreamTest, ReadNetworkResponse) {
network_.InjectRealResponse();
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 10 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, ClearAllAfterLoadResultsInRefresh) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->OnCacheDataCleared(); // triggers ClearAll().
response_translator_.InjectResponse(MakeTypicalInitialModelState());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices -> loading -> 2 slices",
surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, ClearAllWithNoSurfacesAttachedDoesNotReload) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
// Also check that the storage is cleared.
EXPECT_EQ("", DumpStoreState());
}
TEST_F(FeedStreamTest, StorePendingAction) {
stream_->UploadAction(MakeFeedAction(42ul), false, base::DoNothing());
WaitForIdleTaskQueue();
std::vector<feedstore::StoredAction> result =
ReadStoredActions(stream_->GetStore());
ASSERT_EQ(1ul, result.size());
EXPECT_EQ(42ul, result[0].action().content_id().id());
}
TEST_F(FeedStreamTest, StorePendingActionAndUploadNow) {
network_.consistency_token = "token-11";
// Call |ProcessThereAndBackAgain()|, which triggers Upload() with
// upload_now=true.
{
feedwire::ThereAndBackAgainData msg;
*msg.mutable_action_payload() = MakeFeedAction(42ul).action_payload();
stream_->ProcessThereAndBackAgain(msg.SerializeAsString());
}
WaitForIdleTaskQueue();
// Verify the action was uploaded.
EXPECT_EQ(1, network_.action_request_call_count);
std::vector<feedstore::StoredAction> result =
ReadStoredActions(stream_->GetStore());
ASSERT_EQ(0ul, result.size());
}
TEST_F(FeedStreamTest, LoadStreamFromNetworkUploadsActions) {
stream_->UploadAction(MakeFeedAction(99ul), false, base::DoNothing());
WaitForIdleTaskQueue();
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.action_request_call_count);
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
// Uploaded action should have been erased from store.
stream_->UploadAction(MakeFeedAction(100ul), true, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(2, network_.action_request_call_count);
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
}
TEST_F(FeedStreamTest, LoadMoreUploadsActions) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->UploadAction(MakeFeedAction(99ul), false, base::DoNothing());
WaitForIdleTaskQueue();
network_.consistency_token = "token-12";
stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
EXPECT_EQ("token-12", stream_->GetMetadata()->GetConsistencyToken());
// Uploaded action should have been erased from the store.
network_.action_request_sent.reset();
stream_->UploadAction(MakeFeedAction(100ul), true, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
EXPECT_EQ(100ul, network_.action_request_sent->feed_action_request()
.feed_action(0)
.content_id()
.id());
}
TEST_F(FeedStreamTest, UploadActionsOneBatch) {
UploadActions(
{MakeFeedAction(97ul), MakeFeedAction(98ul), MakeFeedAction(99ul)});
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.action_request_call_count);
EXPECT_EQ(
3,
network_.action_request_sent->feed_action_request().feed_action_size());
stream_->UploadAction(MakeFeedAction(99ul), true, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(2, network_.action_request_call_count);
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
}
TEST_F(FeedStreamTest, UploadActionsMultipleBatches) {
UploadActions({
// Batch 1: One really big action.
MakeFeedAction(100ul, /*pad_size=*/20001ul),
// Batch 2
MakeFeedAction(101ul, 10000ul),
MakeFeedAction(102ul, 9000ul),
// Batch 3. Trigger upload.
MakeFeedAction(103ul, 2000ul),
});
WaitForIdleTaskQueue();
EXPECT_EQ(3, network_.action_request_call_count);
stream_->UploadAction(MakeFeedAction(99ul), true, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(4, network_.action_request_call_count);
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
}
TEST_F(FeedStreamTest, UploadActionsSkipsStaleActionsByTimestamp) {
stream_->UploadAction(MakeFeedAction(2ul), false, base::DoNothing());
WaitForIdleTaskQueue();
task_environment_.FastForwardBy(base::TimeDelta::FromHours(25));
// Trigger upload
CallbackReceiver<UploadActionsTask::Result> cr;
stream_->UploadAction(MakeFeedAction(3ul), true, cr.Bind());
WaitForIdleTaskQueue();
// Just one action should have been uploaded.
EXPECT_EQ(1, network_.action_request_call_count);
EXPECT_EQ(
1,
network_.action_request_sent->feed_action_request().feed_action_size());
EXPECT_EQ(3ul, network_.action_request_sent->feed_action_request()
.feed_action(0)
.content_id()
.id());
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(1ul, cr.GetResult()->upload_attempt_count);
EXPECT_EQ(1ul, cr.GetResult()->stale_count);
}
TEST_F(FeedStreamTest, UploadActionsErasesStaleActionsByAttempts) {
// Three failed uploads, plus one more to cause the first action to be erased.
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(0ul), true, base::DoNothing());
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(1ul), true, base::DoNothing());
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(2ul), true, base::DoNothing());
CallbackReceiver<UploadActionsTask::Result> cr;
stream_->UploadAction(MakeFeedAction(3ul), true, cr.Bind());
WaitForIdleTaskQueue();
// Four requests, three pending actions in the last request.
EXPECT_EQ(4, network_.action_request_call_count);
EXPECT_EQ(
3,
network_.action_request_sent->feed_action_request().feed_action_size());
// Action 0 should have been erased.
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(3ul, cr.GetResult()->upload_attempt_count);
EXPECT_EQ(1ul, cr.GetResult()->stale_count);
}
TEST_F(FeedStreamTest, MetadataLoadedWhenDatabaseInitialized) {
ASSERT_TRUE(stream_->GetMetadata());
// Set the token and increment next action ID.
stream_->GetMetadata()->SetConsistencyToken("token");
EXPECT_EQ(0, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue());
// Creating a stream should load metadata.
CreateStream();
ASSERT_TRUE(stream_->GetMetadata());
EXPECT_EQ("token", stream_->GetMetadata()->GetConsistencyToken());
EXPECT_EQ(1, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue());
}
TEST_F(FeedStreamTest, ModelUnloadsAfterTimeout) {
Config config;
config.model_unload_timeout = base::TimeDelta::FromSeconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel());
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2));
WaitForIdleTaskQueue();
EXPECT_FALSE(stream_->GetModel());
}
TEST_F(FeedStreamTest, ModelDoesNotUnloadIfSurfaceIsAttached) {
Config config;
config.model_unload_timeout = base::TimeDelta::FromSeconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel());
surface.Attach(stream_.get());
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel());
}
TEST_F(FeedStreamTest, ModelUnloadsAfterSecondTimeout) {
Config config;
config.model_unload_timeout = base::TimeDelta::FromSeconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel());
// Attaching another surface will prolong the unload time for another second.
surface.Attach(stream_.get());
surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel());
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2));
WaitForIdleTaskQueue();
EXPECT_FALSE(stream_->GetModel());
}
TEST_F(FeedStreamTest, ProvidesPrefetchSuggestionsWhenModelLoaded) {
// Setup by triggering a model load.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Because we loaded from the network,
// PrefetchService::NewSuggestionsAvailable() should have been called.
EXPECT_EQ(1, prefetch_service_.NewSuggestionsAvailableCallCount());
CallbackReceiver<std::vector<offline_pages::PrefetchSuggestion>> callback;
prefetch_service_.suggestions_provider()->GetCurrentArticleSuggestions(
callback.Bind());
WaitForIdleTaskQueue();
ASSERT_TRUE(callback.GetResult());
const std::vector<offline_pages::PrefetchSuggestion>& suggestions =
callback.GetResult().value();
ASSERT_EQ(2UL, suggestions.size());
EXPECT_EQ("http://content0/", suggestions[0].article_url);
EXPECT_EQ("title0", suggestions[0].article_title);
EXPECT_EQ("publisher0", suggestions[0].article_attribution);
EXPECT_EQ("snippet0", suggestions[0].article_snippet);
EXPECT_EQ("http://image0/", suggestions[0].thumbnail_url);
EXPECT_EQ("http://favicon0/", suggestions[0].favicon_url);
EXPECT_EQ("http://content1/", suggestions[1].article_url);
}
TEST_F(FeedStreamTest, ProvidesPrefetchSuggestionsWhenModelNotLoaded) {
store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing());
CallbackReceiver<std::vector<offline_pages::PrefetchSuggestion>> callback;
prefetch_service_.suggestions_provider()->GetCurrentArticleSuggestions(
callback.Bind());
WaitForIdleTaskQueue();
ASSERT_FALSE(stream_->GetModel());
ASSERT_TRUE(callback.GetResult());
const std::vector<offline_pages::PrefetchSuggestion>& suggestions =
callback.GetResult().value();
ASSERT_EQ(2UL, suggestions.size());
EXPECT_EQ("http://content0/", suggestions[0].article_url);
EXPECT_EQ("http://content1/", suggestions[1].article_url);
EXPECT_EQ(0, prefetch_service_.NewSuggestionsAvailableCallCount());
}
TEST_F(FeedStreamTest, ScrubsUrlsInProvidedPrefetchSuggestions) {
{
auto initial_state = MakeTypicalInitialModelState();
initial_state->content[0].mutable_prefetch_metadata(0)->set_uri(
"?notavalidurl?");
initial_state->content[0].mutable_prefetch_metadata(0)->set_image_url(
"?asdf?");
initial_state->content[0].mutable_prefetch_metadata(0)->set_favicon_url(
"?hi?");
initial_state->content[0].mutable_prefetch_metadata(0)->clear_uri();
store_->OverwriteStream(std::move(initial_state), base::DoNothing());
}
CallbackReceiver<std::vector<offline_pages::PrefetchSuggestion>> callback;
prefetch_service_.suggestions_provider()->GetCurrentArticleSuggestions(
callback.Bind());
WaitForIdleTaskQueue();
ASSERT_TRUE(callback.GetResult());
const std::vector<offline_pages::PrefetchSuggestion>& suggestions =
callback.GetResult().value();
ASSERT_EQ(2UL, suggestions.size());
EXPECT_EQ("", suggestions[0].article_url.possibly_invalid_spec());
EXPECT_EQ("", suggestions[0].thumbnail_url.possibly_invalid_spec());
EXPECT_EQ("", suggestions[0].favicon_url.possibly_invalid_spec());
}
TEST_F(FeedStreamTest, OfflineBadgesArePopulatedInitially) {
// Add two offline pages. We exclude tab-bound pages, so only the first is
// used.
offline_page_model_.AddTestPage(GURL("http://content0/"));
offline_page_model_.AddTestPage(GURL("http://content1/"));
offline_page_model_.items()[1].client_id.name_space =
offline_pages::kLastNNamespace;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ((std::map<std::string, std::string>(
{{"app/badge0", SerializedOfflineBadgeContent()}})),
surface.GetDataStoreEntries());
}
TEST_F(FeedStreamTest, OfflineBadgesArePopulatedOnNewOfflineItemAdded) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ((std::map<std::string, std::string>({})),
surface.GetDataStoreEntries());
// Add an offline page.
offline_page_model_.AddTestPage(GURL("http://content1/"));
offline_page_model_.CallObserverOfflinePageAdded(
offline_page_model_.items()[0]);
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
EXPECT_EQ((std::map<std::string, std::string>(
{{"app/badge1", SerializedOfflineBadgeContent()}})),
surface.GetDataStoreEntries());
}
TEST_F(FeedStreamTest, OfflineBadgesAreRemovedWhenOfflineItemRemoved) {
offline_page_model_.AddTestPage(GURL("http://content0/"));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ((std::map<std::string, std::string>(
{{"app/badge0", SerializedOfflineBadgeContent()}})),
surface.GetDataStoreEntries());
// Remove the offline page.
offline_page_model_.CallObserverOfflinePageDeleted(
offline_page_model_.items()[0]);
task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(1));
EXPECT_EQ((std::map<std::string, std::string>()),
surface.GetDataStoreEntries());
}
TEST_F(FeedStreamTest, OfflineBadgesAreProvidedToNewSurfaces) {
offline_page_model_.AddTestPage(GURL("http://content0/"));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
TestSurface surface2(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ((std::map<std::string, std::string>(
{{"app/badge0", SerializedOfflineBadgeContent()}})),
surface2.GetDataStoreEntries());
}
TEST_F(FeedStreamTest, OfflineBadgesAreRemovedWhenModelIsUnloaded) {
offline_page_model_.AddTestPage(GURL("http://content0/"));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->UnloadModel();
// Offline badge no longer present.
EXPECT_EQ((std::map<std::string, std::string>()),
surface.GetDataStoreEntries());
}
TEST_F(FeedStreamTest, MultipleOfflineBadgesWithSameUrl) {
{
std::unique_ptr<StreamModelUpdateRequest> state =
MakeTypicalInitialModelState();
const feedwire::PrefetchMetadata& prefetch_metadata1 =
state->content[0].prefetch_metadata(0);
feedwire::PrefetchMetadata& prefetch_metadata2 =
*state->content[0].add_prefetch_metadata();
prefetch_metadata2 = prefetch_metadata1;
prefetch_metadata2.set_badge_id("app/badge0b");
response_translator_.InjectResponse(std::move(state));
}
offline_page_model_.AddTestPage(GURL("http://content0/"));
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ((std::map<std::string, std::string>(
{{"app/badge0", SerializedOfflineBadgeContent()},
{"app/badge0b", SerializedOfflineBadgeContent()}})),
surface.GetDataStoreEntries());
}
} // namespace
} // namespace feed