summaryrefslogtreecommitdiff
path: root/chromium/components/feed/core/v2/feed_stream_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/components/feed/core/v2/feed_stream_unittest.cc')
-rw-r--r--chromium/components/feed/core/v2/feed_stream_unittest.cc1038
1 files changed, 877 insertions, 161 deletions
diff --git a/chromium/components/feed/core/v2/feed_stream_unittest.cc b/chromium/components/feed/core/v2/feed_stream_unittest.cc
index a514fd7cc63..d0325fbd69e 100644
--- a/chromium/components/feed/core/v2/feed_stream_unittest.cc
+++ b/chromium/components/feed/core/v2/feed_stream_unittest.cc
@@ -4,12 +4,19 @@
#include "components/feed/core/v2/feed_stream.h"
+#include <map>
#include <memory>
+#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"
@@ -19,14 +26,19 @@
#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/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/stream_model_update_request.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/prefs/pref_registry_simple.h"
@@ -42,8 +54,7 @@ std::unique_ptr<StreamModel> LoadModelFromStore(FeedStore* store) {
result = std::move(task_result);
};
LoadStreamFromStoreTask load_task(
- store, /*clock=*/nullptr,
- UserClass::kActiveSuggestionsConsumer, // Has no effect.
+ LoadStreamFromStoreTask::LoadType::kFullLoad, store, /*clock=*/nullptr,
base::BindLambdaForTesting(complete));
// We want to load the data no matter how stale.
load_task.IgnoreStalenessForTesting();
@@ -85,6 +96,22 @@ std::string ModelStateFor(FeedStore* store) {
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());
+}
+
// This is EXPECT_EQ, but also dumps the string values for ease of reading.
#define EXPECT_STRINGS_EQUAL(WANT, GOT) \
{ \
@@ -94,12 +121,42 @@ std::string ModelStateFor(FeedStore* store) {
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 {
- if (!initial_state)
+ 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;
- ++update_count_;
+
+ described_updates_.push_back(CurrentState());
}
// Test functions.
@@ -107,53 +164,67 @@ class TestSurface : public FeedStream::SurfaceInterface {
void Clear() {
initial_state = base::nullopt;
update = base::nullopt;
- update_count_ = 0;
+ described_updates_.clear();
}
- // Describe what is shown on the surface in a format that can be easily
- // asserted against.
- std::string Describe() {
- if (!initial_state)
- return "empty";
-
- if (update->updated_slices().size() == 1 &&
- update->updated_slices()[0].has_slice() &&
- update->updated_slices()[0].slice().has_zero_state_slice()) {
- return "zero-state";
- }
-
- std::stringstream ss;
- ss << update->updated_slices().size() << " slices";
- // If there's more than one update, we want to know that.
- if (update_count_ > 1) {
- ss << " " << update_count_ << " updates";
- }
- return ss.str();
+ // 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;
}
+ // 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:
- int update_count_ = 0;
-};
+ std::string CurrentState() {
+ if (update && IsInitialLoadSpinnerUpdate(*update))
+ return "loading";
-class TestUserClassifier : public UserClassifier {
- public:
- TestUserClassifier(PrefService* pref_service, const base::Clock* clock)
- : UserClassifier(pref_service, clock) {}
- // UserClassifier.
- UserClass GetUserClass() const override {
- return overridden_user_class_.value_or(UserClassifier::GetUserClass());
+ 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();
}
- // Test use.
- void OverrideUserClass(UserClass user_class) {
- overridden_user_class_ = user_class;
+ 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();
}
- private:
- base::Optional<UserClass> overridden_user_class_;
+ // The stream if it was attached using the constructor.
+ FeedStream* stream_ = nullptr;
+ std::vector<std::string> described_updates_;
};
class TestFeedNetwork : public FeedNetwork {
@@ -168,79 +239,160 @@ class TestFeedNetwork : public FeedNetwork {
// time we want to inject a translated response for ease of test-writing.
query_request_sent = request;
QueryRequestResult result;
- result.status_code = 200;
- result.response_body = std::make_unique<feedwire::Response>();
+ 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 {
- NOTIMPLEMENTED();
+ 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:
- std::unique_ptr<StreamModelUpdateRequest> TranslateWireResponse(
+ RefreshResponseData TranslateWireResponse(
feedwire::Response response,
- base::TimeDelta response_time,
+ StreamModelUpdateRequest::Source source,
base::Time current_time) override {
if (injected_response_) {
- return std::move(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), response_time, current_time);
+ std::move(response), source, current_time);
}
void InjectResponse(std::unique_ptr<StreamModelUpdateRequest> response) {
- injected_response_ = std::move(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:
- std::unique_ptr<StreamModelUpdateRequest> injected_response_;
+ base::Optional<RefreshResponseData> injected_response_;
};
class FakeRefreshTaskScheduler : public RefreshTaskScheduler {
public:
// RefreshTaskScheduler implementation.
- void EnsureScheduled(base::TimeDelta period) override {
- scheduled_period = period;
+ void EnsureScheduled(base::TimeDelta run_time) override {
+ scheduled_run_time = run_time;
}
void Cancel() override { canceled = true; }
void RefreshTaskComplete() override { refresh_task_complete = true; }
- base::Optional<base::TimeDelta> scheduled_period;
+ 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 TestEventObserver : public FeedStream::EventObserver {
+class TestMetricsReporter : public MetricsReporter {
public:
- // FeedStreamUnittest::StreamEventObserver.
+ explicit TestMetricsReporter(const base::TickClock* clock)
+ : MetricsReporter(clock) {}
+
+ // 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 OnMaybeTriggerRefresh(TriggerType trigger,
- bool clear_all_before_refresh) override {
- refresh_trigger_type = trigger;
+ 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;
};
@@ -251,21 +403,8 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate {
feed::prefs::RegisterFeedSharedProfilePrefs(profile_prefs_.registry());
feed::RegisterProfilePrefs(profile_prefs_.registry());
CHECK_EQ(kTestTimeEpoch, task_environment_.GetMockClock()->Now());
- stream_ = std::make_unique<FeedStream>(
- &refresh_scheduler_, &event_observer_, this, &profile_prefs_, &network_,
- store_.get(), task_environment_.GetMockClock(),
- task_environment_.GetMockTickClock(),
- task_environment_.GetMainThreadTaskRunner());
-
- // Set the user classifier.
- auto user_classifier = std::make_unique<TestUserClassifier>(
- &profile_prefs_, task_environment_.GetMockClock());
- user_classifier_ = user_classifier.get();
- stream_->SetUserClassifierForTesting(std::move(user_classifier));
-
- WaitForIdleTaskQueue(); // Wait for any initialization.
- stream_->SetWireResponseTranslatorForTesting(&response_translator_);
+ CreateStream();
}
void TearDown() override {
@@ -280,9 +419,31 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate {
// 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_, this, &profile_prefs_,
+ &network_, store_.get(), 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();
@@ -300,14 +461,41 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate {
void UnloadModel() {
WaitForIdleTaskQueue();
- stream_->UnloadModelForTesting();
+ 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};
- TestUserClassifier* user_classifier_;
- TestEventObserver event_observer_;
+ TestMetricsReporter metrics_reporter_{task_environment_.GetMockTickClock()};
TestingPrefServiceSimple profile_prefs_;
TestFeedNetwork network_;
TestWireResponseTranslator response_translator_;
@@ -335,28 +523,55 @@ TEST_F(FeedStreamTest, SetArticlesListVisible) {
EXPECT_TRUE(stream_->IsArticlesListVisible());
}
-TEST_F(FeedStreamTest, RefreshIsScheduledOnInitialize) {
+TEST_F(FeedStreamTest, DoNotRefreshIfArticlesListIsHidden) {
+ stream_->SetArticlesListVisible(false);
stream_->InitializeScheduling();
- EXPECT_TRUE(refresh_scheduler_.scheduled_period);
+ 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, ScheduledRefreshTriggersRefresh) {
- stream_->InitializeScheduling();
+TEST_F(FeedStreamTest, BackgroundRefreshSuccess) {
+ // Trigger a background refresh.
+ response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask();
+ WaitForIdleTaskQueue();
- EXPECT_EQ(TriggerType::kFixedTimer, event_observer_.refresh_trigger_type);
- // TODO(harringtond): Once we actually perform the refresh, make sure
- // RefreshTaskComplete() is called.
- // EXPECT_TRUE(refresh_scheduler_.refresh_task_complete);
+ // 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());
}
-TEST_F(FeedStreamTest, DoNotRefreshIfArticlesListIsHidden) {
- stream_->SetArticlesListVisible(false);
- stream_->InitializeScheduling();
+TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedWhenModelIsLoading) {
+ response_translator_.InjectResponse(MakeTypicalInitialModelState());
+ TestSurface surface(stream_.get());
stream_->ExecuteRefreshTask();
+ WaitForIdleTaskQueue();
- EXPECT_TRUE(refresh_scheduler_.canceled);
- EXPECT_FALSE(event_observer_.refresh_trigger_type);
+ 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) {
@@ -365,8 +580,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesInitialContent) {
model->Update(MakeTypicalInitialModelState());
stream_->LoadModelForTesting(std::move(model));
}
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ 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());
@@ -386,8 +600,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesInitialContent) {
}
TEST_F(FeedStreamTest, SurfaceReceivesInitialContentLoadedAfterAttach) {
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
ASSERT_FALSE(surface.initial_state);
{
auto model = std::make_unique<StreamModel>();
@@ -395,7 +608,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesInitialContentLoadedAfterAttach) {
stream_->LoadModelForTesting(std::move(model));
}
- ASSERT_EQ("2 slices", surface.Describe());
+ 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());
@@ -419,8 +632,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesUpdatedContent) {
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(std::move(model));
}
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
// Remove #1, add #2.
stream_->ExecuteOperations({
MakeOperation(MakeRemove(MakeClusterId(1))),
@@ -432,7 +644,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesUpdatedContent) {
const feedui::StreamUpdate& initial_state = surface.initial_state.value();
const feedui::StreamUpdate& update = surface.update.value();
- ASSERT_EQ("2 slices 2 updates", surface.Describe());
+ 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());
@@ -448,8 +660,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesSecondUpdatedContent) {
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(std::move(model));
}
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
// Add #2.
stream_->ExecuteOperations({
MakeOperation(MakeCluster(2, MakeRootId())),
@@ -466,7 +677,7 @@ TEST_F(FeedStreamTest, SurfaceReceivesSecondUpdatedContent) {
// The last update should have only one new piece of content.
// This verifies the current content set is tracked properly.
- ASSERT_EQ("4 slices 3 updates", surface.Describe());
+ 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());
@@ -478,16 +689,29 @@ TEST_F(FeedStreamTest, SurfaceReceivesSecondUpdatedContent) {
.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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
EXPECT_TRUE(surface.initial_state);
- stream_->DetachSurface(&surface);
+ surface.Detach();
surface.Clear();
// Arbitrary stream change. Surface should not see the update.
@@ -498,40 +722,109 @@ TEST_F(FeedStreamTest, DetachSurface) {
}
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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
- EXPECT_TRUE(network_.query_request_sent);
+ 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("2 slices", surface.Describe());
+
+ 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(store_.get()),
- ModelStateFor(MakeTypicalInitialModelState()));
+ 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.
- user_classifier_->OverrideUserClass(UserClass::kActiveSuggestionsConsumer);
- store_->SaveFullStream(MakeTypicalInitialModelState(
-
- kTestTimeEpoch - base::TimeDelta::FromHours(12) -
- base::TimeDelta::FromMinutes(1)),
- base::DoNothing());
+ 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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
- EXPECT_TRUE(network_.query_request_sent);
+ 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);
}
@@ -540,48 +833,60 @@ 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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed,
- event_observer_.load_stream_status);
+ metrics_reporter_.load_stream_status);
}
TEST_F(FeedStreamTest, DoNotLoadFromNetworkWhenOffline) {
is_offline_ = true;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkOffline,
- event_observer_.load_stream_status);
- EXPECT_EQ("zero-state", surface.Describe());
+ 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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden,
- event_observer_.load_stream_status);
- EXPECT_EQ("zero-state", surface.Describe());
+ 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_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedEulaNotAccepted,
- event_observer_.load_stream_status);
- EXPECT_EQ("zero-state", surface.Describe());
+ 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) {
@@ -589,21 +894,21 @@ TEST_F(FeedStreamTest, DoNotLoadFromNetworkAfterHistoryIsDeleted) {
task_environment_.FastForwardBy(kSuppressRefreshDuration -
base::TimeDelta::FromSeconds(1));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
- EXPECT_EQ("zero-state", surface.Describe());
+ EXPECT_EQ("loading -> no-cards", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete,
- event_observer_.load_stream_status);
+ metrics_reporter_.load_stream_status);
- stream_->DetachSurface(&surface);
+ surface.Detach();
task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2));
- stream_->AttachSurface(&surface);
+ surface.Clear();
+ surface.Attach(stream_.get());
WaitForIdleTaskQueue();
- EXPECT_EQ("2 slices 2 updates", surface.Describe());
+ EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) {
@@ -618,54 +923,59 @@ TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) {
TEST_F(FeedStreamTest, LoadStreamFromStore) {
// Fill the store with stream data that is just barely fresh, and verify it
// loads.
- user_classifier_->OverrideUserClass(UserClass::kActiveSuggestionsConsumer);
- store_->SaveFullStream(MakeTypicalInitialModelState(
- kTestTimeEpoch - base::TimeDelta::FromHours(12) +
- base::TimeDelta::FromMinutes(1)),
- base::DoNothing());
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ 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("2 slices", surface.Describe());
+ 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_->AttachSurface(&surface);
- stream_->DetachSurface(&surface);
+ TestSurface surface(stream_.get());
+ surface.Detach();
WaitForIdleTaskQueue();
- EXPECT_EQ("empty", surface.Describe());
+ EXPECT_EQ("loading", surface.DescribeUpdates());
EXPECT_TRUE(network_.query_request_sent);
}
TEST_F(FeedStreamTest, AttachMultipleSurfacesLoadsModelOnce) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
- TestSurface surface;
- TestSurface other_surface;
- stream_->AttachSurface(&surface);
- stream_->AttachSurface(&other_surface);
+ 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.
- TestSurface later_surface;
- stream_->AttachSurface(&later_surface);
+ // 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_->SaveFullStream(MakeTypicalInitialModelState(), base::DoNothing());
- TestSurface surface;
- stream_->AttachSurface(&surface);
+ store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing());
+ TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(surface.initial_state);
@@ -687,10 +997,10 @@ TEST_F(FeedStreamTest, ModelChangesAreSavedToStorage) {
// Unload and reload the model from the store, and verify we can still apply
// operations correctly.
- stream_->DetachSurface(&surface);
+ surface.Detach();
surface.Clear();
UnloadModel();
- stream_->AttachSurface(&surface);
+ surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(surface.initial_state);
@@ -709,5 +1019,411 @@ TEST_F(FeedStreamTest, ModelChangesAreSavedToStorage) {
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());
+ // 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());
+ // 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());
+}
+
+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, 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";
+
+ CallbackReceiver<UploadActionsTask::Result> cr;
+ stream_->UploadAction(MakeFeedAction(42ul), true, cr.Bind());
+ WaitForIdleTaskQueue();
+
+ ASSERT_TRUE(cr.GetResult());
+ EXPECT_EQ(1ul, cr.GetResult()->upload_attempt_count);
+ EXPECT_EQ(UploadActionsStatus::kUpdatedConsistencyToken,
+ cr.GetResult()->status);
+
+ 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());
+}
+
} // namespace
} // namespace feed