blob: 2cd1eeb897e0fdcafb999a8be8710fd00d6532c1 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/feed/core/common/pref_names.h"
#include "components/feed/core/proto/v2/wire/feed_entry_point_source.pb.h"
#include "components/feed/core/proto/v2/wire/feed_query.pb.h"
#include "components/feed/core/proto/v2/wire/info_card.pb.h"
#include "components/feed/core/shared_prefs/pref_names.h"
#include "components/feed/core/v2/api_test/feed_api_test.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/enums.h"
#include "components/feed/core/v2/feed_network.h"
#include "components/feed/core/v2/feed_stream.h"
#include "components/feed/core/v2/feedstore_util.h"
#include "components/feed/core/v2/prefs.h"
#include "components/feed/core/v2/public/feed_api.h"
#include "components/feed/core/v2/public/feed_service.h"
#include "components/feed/core/v2/public/stream_type.h"
#include "components/feed/core/v2/public/types.h"
#include "components/feed/core/v2/scheduling.h"
#include "components/feed/core/v2/test/callback_receiver.h"
#include "components/feed/core/v2/test/stream_builder.h"
#include "components/feed/feed_feature_list.h"
#include "feed_api_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
// This file is primarily for testing access to the Feed content, though it
// contains some other miscellaneous tests.
namespace feed {
namespace test {
namespace {
using ::feedwire::webfeed::WebFeedChangeReason;
const int kTestInfoCardType1 = 101;
const int kTestInfoCardType2 = 8888;
const int kMinimumViewIntervalSeconds = 5 * 60;
TEST_F(FeedApiTest, IsArticlesListVisibleByDefault) {
EXPECT_TRUE(stream_->IsArticlesListVisible());
}
TEST_F(FeedApiTest, DoNotRefreshIfArticlesListIsHidden) {
profile_prefs_.SetBoolean(prefs::kArticlesListVisible, false);
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
EXPECT_FALSE(refresh_scheduler_.scheduled_run_times.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(std::set<RefreshTaskId>({RefreshTaskId::kRefreshForYouFeed}),
refresh_scheduler_.completed_tasks);
}
TEST_F(FeedApiTest, BackgroundRefreshForYouSuccess) {
// Trigger a background refresh.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// Verify the refresh happened and that we can load a stream without the
// network.
ASSERT_TRUE(refresh_scheduler_.completed_tasks.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->background_refresh_status);
EXPECT_TRUE(network_.query_request_sent);
EXPECT_EQ(feedwire::FeedQuery::SCHEDULED_REFRESH,
network_.query_request_sent->feed_request().feed_query().reason());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
EXPECT_FALSE(stream_->GetModel(StreamType(StreamKind::kForYou)));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, WebFeedDoesNotBackgroundRefresh) {
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
RequestSchedule schedule;
schedule.anchor_time = kTestTimeEpoch;
schedule.refresh_offsets = {base::Seconds(12), base::Seconds(48)};
injected_response.request_schedule = schedule;
response_translator_.InjectResponse(std::move(injected_response));
}
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
// The request schedule should be ignored.
EXPECT_TRUE(refresh_scheduler_.scheduled_run_times.empty());
}
TEST_F(FeedApiTest, BackgroundRefreshPrefetchesImages) {
// Trigger a background refresh.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
EXPECT_EQ(0, prefetch_image_call_count_);
WaitForIdleTaskQueue();
std::vector<GURL> expected_fetches(
{GURL("http://image0/"), GURL("http://favicon0/"), GURL("http://image1/"),
GURL("http://favicon1/")});
// Verify that images were prefetched.
EXPECT_EQ(4, prefetch_image_call_count_);
EXPECT_EQ(expected_fetches, prefetched_images_);
}
TEST_F(FeedApiTest, BackgroundRefreshNotAttemptedWhenModelIsLoading) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
EXPECT_EQ(metrics_reporter_->Stream(StreamType(StreamKind::kForYou))
.background_refresh_status,
LoadStreamStatus::kModelAlreadyLoaded);
}
TEST_F(FeedApiTest, BackgroundRefreshNotAttemptedAfterModelIsLoaded) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
EXPECT_EQ(metrics_reporter_->background_refresh_status,
LoadStreamStatus::kModelAlreadyLoaded);
}
TEST_F(FeedApiTest, SurfaceReceivesInitialContent) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
// Use `the_first_surface` to force loading content.
TestForYouSurface the_first_surface(stream_.get());
WaitForIdleTaskQueue();
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
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());
EXPECT_TRUE(initial_state.logging_parameters().logging_enabled());
EXPECT_EQ(MakeRootEventId(),
initial_state.logging_parameters().root_event_id());
}
TEST_F(FeedApiTest, SurfaceReceivesInitialContentLoadedAfterAttach) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
ASSERT_FALSE(surface.initial_state);
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 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(FeedApiTest, SurfaceReceivesUpdatedContent) {
{
auto model = CreateStreamModel();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(StreamType(StreamKind::kForYou),
std::move(model));
}
TestForYouSurface surface(stream_.get());
// Remove #1, add #2.
stream_->ExecuteOperations(
StreamType(StreamKind::kForYou),
{
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(FeedApiTest, SurfaceReceivesSecondUpdatedContent) {
{
auto model = CreateStreamModel();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(StreamType(StreamKind::kForYou),
std::move(model));
}
TestForYouSurface surface(stream_.get());
// Add #2.
stream_->ExecuteOperations(
StreamType(StreamKind::kForYou),
{
MakeOperation(MakeCluster(2, MakeRootId())),
MakeOperation(MakeContentNode(2, MakeClusterId(2))),
MakeOperation(MakeContent(2)),
});
// Clear the last update and add #3.
stream_->ExecuteOperations(
StreamType(StreamKind::kForYou),
{
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(FeedApiTest, RemoveAllContentResultsInZeroState) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Remove both pieces of content.
stream_->ExecuteOperations(StreamType(StreamKind::kForYou),
{
MakeOperation(MakeRemove(MakeClusterId(0))),
MakeOperation(MakeRemove(MakeClusterId(1))),
});
ASSERT_EQ("loading -> [user@foo] 2 slices -> no-cards",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, DetachSurface) {
{
auto model = CreateStreamModel();
model->ExecuteOperations(MakeTypicalStreamOperations());
stream_->LoadModelForTesting(StreamType(StreamKind::kForYou),
std::move(model));
}
TestForYouSurface surface(stream_.get());
EXPECT_TRUE(surface.initial_state);
surface.Detach();
surface.Clear();
// Arbitrary stream change. Surface should not see the update.
stream_->ExecuteOperations(StreamType(StreamKind::kForYou),
{
MakeOperation(MakeRemove(MakeClusterId(1))),
});
EXPECT_FALSE(surface.update);
}
TEST_F(FeedApiTest, FetchImage) {
CallbackReceiver<NetworkResponse> receiver;
stream_->FetchImage(GURL("https://example.com"), receiver.Bind());
EXPECT_EQ("dummyresponse", receiver.GetResult()->response_bytes);
}
TEST_F(FeedStreamTestForAllStreamTypes,
ReportContentLifetimeMetricsViaOnLoadStream) {
base::HistogramTester histograms;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
feedstore::Metadata::StreamMetadata::ContentLifetime content_lifetime;
content_lifetime.set_stale_age_ms(10);
content_lifetime.set_invalid_age_ms(11);
injected_response.content_lifetime = std::move(content_lifetime);
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
ASSERT_TRUE(network_.query_request_sent);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAgeIsPresent", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAgeIsPresent", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAge", 10, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAge", 11, 1);
}
TEST_F(FeedStreamTestForAllStreamTypes,
DoNotReportContentLifetimeMetricsWhenStreamMetadataNotPopulated) {
base::HistogramTester histograms;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
ASSERT_TRUE(network_.query_request_sent);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAgeIsPresent", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAgeIsPresent", false, 1);
}
TEST_F(FeedStreamTestForAllStreamTypes,
DoNotReportContentLifetimeStaleAgeWhenNotPopulated) {
base::HistogramTester histograms;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
feedstore::Metadata::StreamMetadata::ContentLifetime content_lifetime;
content_lifetime.set_invalid_age_ms(11);
injected_response.content_lifetime = std::move(content_lifetime);
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
ASSERT_TRUE(network_.query_request_sent);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAgeIsPresent", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAgeIsPresent", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAge", 11, 1);
}
TEST_F(FeedStreamTestForAllStreamTypes,
DoNotReportContentLifetimeInvalidAgeWhenNotPopulated) {
base::HistogramTester histograms;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
feedstore::Metadata::StreamMetadata::ContentLifetime content_lifetime;
content_lifetime.set_stale_age_ms(10);
injected_response.content_lifetime = std::move(content_lifetime);
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
ASSERT_TRUE(network_.query_request_sent);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAgeIsPresent", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.InvalidAgeIsPresent", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentLifetime.StaleAge", 10, 1);
}
TEST_P(FeedStreamTestForAllStreamTypes, LoadFromNetwork) {
{
WaitForIdleTaskQueue();
auto metadata = stream_->GetMetadata();
metadata.set_consistency_token("token");
stream_->SetMetadata(metadata);
}
// 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(0, network_.GetApiRequestCount<QueryInteractiveFeedDiscoverApi>());
EXPECT_EQ(
"token",
network_.query_request_sent->feed_request().consistency_token().token());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Verify the model is filled correctly.
EXPECT_STRINGS_EQUAL(
ModelStateFor(MakeTypicalInitialModelState()),
stream_->GetModel(GetStreamType())->DumpStateForTesting());
// Verify the data was written to the store.
EXPECT_STRINGS_EQUAL(ModelStateFor(MakeTypicalInitialModelState()),
ModelStateFor(GetStreamType(), store_.get()));
}
TEST_P(FeedStreamTestForAllStreamTypes, UseFeedQueryOverride) {
Config config = GetFeedConfig();
config.use_feed_query_requests = true;
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
// There should be no API refresh requests when using use_feed_query_requests.
auto api_request_counts = network_.GetApiRequestCounts();
api_request_counts.erase(NetworkRequestType::kListWebFeeds); // ignore
EXPECT_EQ((std::map<NetworkRequestType, int>()), api_request_counts);
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, OnboardingFetchAfterStartup) {
// Enable WebFeed and WebFeedOnboarding flags.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeed,
kWebFeedOnboarding},
disabled_features = {};
response_translator_.InjectResponse(MakeTypicalInitialModelState());
features.InitWithFeatures(enabled_features, disabled_features);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// There should have been a fetch, even though there are no subscriptions.
ASSERT_TRUE(network_.query_request_sent);
ASSERT_EQ(1, network_.GetWebFeedListContentsCount());
}
TEST_F(FeedApiTest, WebFeedLoadWithNoSubscriptions) {
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> no-subscriptions", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, WebFeedLoadWithNoSubscriptionsAndOnboarding) {
// Turn on the onboarding feature.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeedOnboarding},
disabled_features = {};
response_translator_.InjectResponse(MakeTypicalInitialModelState());
features.InitWithFeatures(enabled_features, disabled_features);
// Scopes are to control the lifetime of the surface object.
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
}
// The initial fetch should work fine.
ASSERT_EQ(1, network_.GetWebFeedListContentsCount());
// Prepare the next fetch response.
response_translator_.InjectResponse(MakeTypicalNextPageState());
// Make the content a bit less than the stale threshold, and make sure we
// don't fetch.
task_environment_.FastForwardBy(base::Days(6));
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
}
ASSERT_EQ(1, network_.GetWebFeedListContentsCount());
// Make the content as stale as the threshold, and make sure we do fetch.
task_environment_.FastForwardBy(base::Days(2));
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
}
ASSERT_EQ(2, network_.GetWebFeedListContentsCount());
}
TEST_F(FeedApiTest, WebFeedContentExprirationWithNoSubscriptionsAndOnboarding) {
// Turn on the onboarding feature.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeedOnboarding},
disabled_features = {};
response_translator_.InjectResponse(MakeTypicalInitialModelState());
features.InitWithFeatures(enabled_features, disabled_features);
// Scopes are to control the lifetime of the surface object.
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
// The initial fetch should work fine.
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
// Don't prepare a fetch response to simulate fetch failure.
// Make the content a bit less than the expired threshold, and make sure we
// load the stale, but expired content.
task_environment_.FastForwardBy(base::Days(13));
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
// Make the content expired, and make sure we don't load it.
task_environment_.FastForwardBy(base::Days(2));
{
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
}
}
// Test that we use QueryInteractiveFeedDiscoverApi and QueryNextPageDiscoverApi
// when kDiscoFeedEndpoint is enabled.
TEST_F(FeedApiTest, LoadFromNetworkDiscoFeedEnabled) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeature(kDiscoFeedEndpoint);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalNextPageState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetApiRequestCount<QueryInteractiveFeedDiscoverApi>());
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetApiRequestCount<QueryNextPageDiscoverApi>());
EXPECT_EQ("loading -> [user@foo] 2 slices -> 2 slices +spinner -> 4 slices",
surface.DescribeUpdates());
}
TEST_P(FeedNetworkEndpointTest, TestAllNetworkEndpointConfigs) {
SetUseFeedQueryRequests(GetUseFeedQueryRequests());
// Enable WebFeed and subscribe to a page, so that we can check if the WebFeed
// is refreshed by ForceRefreshForDebugging.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeed},
disabled_features = {};
if (GetDiscoFeedEnabled()) {
enabled_features.push_back(kDiscoFeedEndpoint);
} else {
disabled_features.push_back(kDiscoFeedEndpoint);
}
features.InitWithFeatures(enabled_features, disabled_features);
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
// Force a refresh that results in a successful load of both feed types.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("[user@foo] 2 slices", surface.DescribeState());
EXPECT_EQ("[user@foo] 2 slices", web_feed_surface.DescribeState());
// Total 2 queries (Web + For You).
EXPECT_EQ(2, network_.send_query_call_count);
// API request to DiscoFeed (For You) - if enabled by feature
EXPECT_EQ((!GetUseFeedQueryRequests() && GetDiscoFeedEnabled()) ? 1 : 0,
network_.GetApiRequestCount<QueryInteractiveFeedDiscoverApi>());
// API request to WebFeedList if FeedQuery not enabled by config.
EXPECT_EQ(GetUseFeedQueryRequests() ? 0 : 1,
network_.GetApiRequestCount<WebFeedListContentsDiscoverApi>());
}
// Perform a background refresh when DiscoFeedEndpoint is enabled. A
// QueryBackgroundFeedDiscoverApi request should be made.
TEST_F(FeedApiTest, BackgroundRefreshDiscoFeedEnabled) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeature(kDiscoFeedEndpoint);
// Trigger a background refresh.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// Verify the refresh happened and that we can load a stream without the
// network.
ASSERT_TRUE(refresh_scheduler_.completed_tasks.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(1, network_.GetApiRequestCount<QueryBackgroundFeedDiscoverApi>());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->background_refresh_status);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_P(FeedStreamTestForAllStreamTypes, ForceRefreshForDebugging) {
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ForceRefreshForDebugging(GetStreamType());
WaitForIdleTaskQueue();
is_offline_ = true;
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("[user@foo] 2 slices", surface.DescribeState());
}
TEST_F(FeedApiTest, RefreshScheduleFlow) {
// Inject a typical network response, with a server-defined request schedule.
{
RequestSchedule schedule;
schedule.anchor_time = kTestTimeEpoch;
schedule.refresh_offsets = {base::Seconds(12), base::Seconds(48)};
RefreshResponseData response_data;
response_data.model_update_request = MakeTypicalInitialModelState();
response_data.request_schedule = schedule;
response_translator_.InjectResponse(std::move(response_data));
// Load the stream, and then destroy the surface to allow background
// refresh.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
UnloadModel(surface.GetStreamType());
}
// Verify the first refresh was scheduled.
EXPECT_EQ(base::Seconds(12),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Simulate executing the background task.
refresh_scheduler_.Clear();
task_environment_.AdvanceClock(base::Seconds(12));
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// Verify |RefreshTaskComplete()| was called and next refresh was scheduled.
EXPECT_TRUE(refresh_scheduler_.completed_tasks.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(base::Seconds(48 - 12),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Simulate executing the background task again.
refresh_scheduler_.Clear();
task_environment_.AdvanceClock(base::Seconds(48 - 12));
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// Verify |RefreshTaskComplete()| was called and next refresh was scheduled.
EXPECT_TRUE(refresh_scheduler_.completed_tasks.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(GetFeedConfig().default_background_refresh_interval,
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, ForceRefreshIfMissedScheduledRefresh) {
// Inject a typical network response, with a server-defined request schedule.
{
RequestSchedule schedule;
schedule.anchor_time = kTestTimeEpoch;
schedule.refresh_offsets = {base::Seconds(12), base::Seconds(48)};
RefreshResponseData response_data;
response_data.model_update_request = MakeTypicalInitialModelState();
response_data.request_schedule = schedule;
response_translator_.InjectResponse(std::move(response_data));
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
surface.Detach();
stream_->UnloadModel(surface.GetStreamType());
// Ensure a refresh is foreced only after a scheduled refresh was missed.
// First, load the stream after 11 seconds.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
task_environment_.AdvanceClock(base::Seconds(11));
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count); // no refresh yet
// Load the stream after 13 seconds. We missed the scheduled refresh at
// 12 seconds.
surface.Detach();
stream_->UnloadModel(surface.GetStreamType());
task_environment_.AdvanceClock(base::Seconds(2));
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
EXPECT_EQ(LoadStreamStatus::kDataInStoreStaleMissedLastRefresh,
metrics_reporter_->load_stream_from_store_status);
}
TEST_F(FeedApiTest, LoadFromNetworkBecauseStoreIsStale_NetworkStaleAge) {
base::TimeDelta default_staleness_threshold =
GetFeedConfig().GetStalenessThreshold(StreamType(StreamKind::kForYou),
/*is_web_feed_subscriber=*/true);
base::TimeDelta server_staleness_threshold = default_staleness_threshold / 2;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
feedstore::Metadata::StreamMetadata::ContentLifetime content_lifetime;
content_lifetime.set_stale_age_ms(
server_staleness_threshold.InMilliseconds());
injected_response.content_lifetime = std::move(content_lifetime);
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
// Fast forward enough to pass the server stale age but not the default stale
// age.
task_environment_.FastForwardBy(server_staleness_threshold +
base::Seconds(1));
// Set up the response translator to be prepared for another request (which we
// expect to happen).
response_translator_.InjectResponse(MakeTypicalInitialModelState());
ASSERT_FALSE(response_translator_.InjectedResponseConsumed());
CreateStream();
// Store is stale, so we should fallback to a network request.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_TRUE(surface.initial_state);
}
TEST_F(FeedApiTest, LoadFromNetworkBecauseStoreIsExpired_NetworkExpiredAge) {
base::HistogramTester histograms;
base::TimeDelta default_content_expiration_threshold =
GetFeedConfig().content_expiration_threshold;
base::TimeDelta server_content_expiration_threshold =
default_content_expiration_threshold / 2;
{
RefreshResponseData injected_response;
injected_response.model_update_request = MakeTypicalInitialModelState();
feedstore::Metadata::StreamMetadata::ContentLifetime content_lifetime;
content_lifetime.set_invalid_age_ms(
server_content_expiration_threshold.InMilliseconds());
injected_response.content_lifetime = std::move(content_lifetime);
response_translator_.InjectResponse(std::move(injected_response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(response_translator_.InjectedResponseConsumed());
}
base::TimeDelta content_age =
server_content_expiration_threshold + base::Seconds(1);
// Fast forward enough to pass the server expiration age but not the default
// expiration age.
task_environment_.FastForwardBy(content_age);
// Set up the response translator to be prepared for another request (which we
// expect to happen).
response_translator_.InjectResponse(MakeTypicalInitialModelState());
ASSERT_FALSE(response_translator_.InjectedResponseConsumed());
CreateStream();
// Store is stale, so we should fallback to a network request.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_TRUE(surface.initial_state);
EXPECT_EQ(LoadStreamStatus::kDataInStoreIsExpired,
metrics_reporter_->load_stream_from_store_status);
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ContentAgeOnLoad.BlockingRefresh", content_age,
1);
}
TEST_P(FeedStreamTestForAllStreamTypes, LoadFromNetworkBecauseStoreIsStale) {
// Fill the store with stream data that is just barely stale, and verify we
// fetch new data over the network.
store_->OverwriteStream(
GetStreamType(),
MakeTypicalInitialModelState(
/*first_cluster_id=*/0,
kTestTimeEpoch -
GetFeedConfig().GetStalenessThreshold(
GetStreamType(), /*is_web_feed_subscriber=*/true) -
base::Minutes(1)),
base::DoNothing());
// 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);
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_TRUE(surface.initial_state);
}
// Same as LoadFromNetworkBecauseStoreIsStale, but with expired content.
TEST_F(FeedApiTest, LoadFromNetworkBecauseStoreIsExpired) {
base::HistogramTester histograms;
const base::TimeDelta kContentAge =
GetFeedConfig().content_expiration_threshold + base::Minutes(1);
store_->OverwriteStream(
StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(
/*first_cluster_id=*/0, kTestTimeEpoch - kContentAge),
base::DoNothing());
// Store is stale, so we should fallback to a network request.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_TRUE(surface.initial_state);
EXPECT_EQ(LoadStreamStatus::kDataInStoreIsExpired,
metrics_reporter_->load_stream_from_store_status);
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ContentAgeOnLoad.BlockingRefresh", kContentAge,
1);
}
TEST_F(FeedApiTest, LoadStaleDataBecauseNetworkRequestFails) {
// Fill the store with stream data that is just barely stale.
base::HistogramTester histograms;
const base::TimeDelta kContentAge =
GetFeedConfig().stale_content_threshold + base::Minutes(1);
store_->OverwriteStream(
StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(
/*first_cluster_id=*/0, kTestTimeEpoch - kContentAge),
base::DoNothing());
// Store is stale, so we should fallback to a network request. Since we didn't
// inject a network response, the network update will fail.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kDataInStoreIsStale,
metrics_reporter_->load_stream_from_store_status);
EXPECT_EQ(LoadStreamStatus::kLoadedStaleDataFromStoreDueToNetworkFailure,
metrics_reporter_->load_stream_status);
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ContentAgeOnLoad.NotRefreshed", kContentAge, 1);
}
TEST_P(FeedStreamTestForAllStreamTypes, LoadFailsStoredDataIsExpired) {
// Fill the store with stream data that is just barely expired.
store_->OverwriteStream(
GetStreamType(),
MakeTypicalInitialModelState(
/*first_cluster_id=*/0,
kTestTimeEpoch - GetFeedConfig().content_expiration_threshold -
base::Minutes(1)),
base::DoNothing());
// Store contains expired content, so we should fallback to a network request.
// Since we didn't inject a network response, the network update will fail.
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent);
EXPECT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kDataInStoreIsExpired,
metrics_reporter_->load_stream_from_store_status);
EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed,
metrics_reporter_->load_stream_status);
}
TEST_F(FeedApiTest, 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.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed,
metrics_reporter_->load_stream_status);
}
TEST_F(FeedApiTest, DoNotLoadFromNetworkWhenOffline) {
is_offline_ = true;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkOffline,
metrics_reporter_->load_stream_status);
EXPECT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, DoNotLoadStreamWhenArticleListIsHidden) {
profile_prefs_.SetBoolean(prefs::kArticlesListVisible, false);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden,
metrics_reporter_->load_stream_status);
EXPECT_EQ("no-cards", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, DoNotLoadStreamWhenEulaIsNotAccepted) {
is_eula_accepted_ = false;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedEulaNotAccepted,
metrics_reporter_->load_stream_status);
EXPECT_EQ("no-cards", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, LoadStreamAfterEulaIsAccepted) {
// Connect a surface before the EULA is accepted.
is_eula_accepted_ = false;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface 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 -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, WebFeedUsesSignedInRequestAfterHistoryIsDeleted) {
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
stream_->OnAllHistoryDeleted();
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_NE(AccountInfo{}, network_.last_account_info);
}
TEST_F(FeedApiTest, ShouldMakeFeedQueryRequestConsumesQuota) {
LoadStreamStatus status = LoadStreamStatus::kNoStatus;
for (; status == LoadStreamStatus::kNoStatus;
status = stream_
->ShouldMakeFeedQueryRequest(
StreamType(StreamKind::kForYou), LoadType::kInitialLoad)
.load_stream_status) {
}
ASSERT_EQ(LoadStreamStatus::kCannotLoadFromNetworkThrottled, status);
}
TEST_F(FeedApiTest, LoadStreamFromStore) {
// Fill the store with stream data that is just barely fresh, and verify it
// loads.
store_->OverwriteStream(
StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(
/*first_cluster_id=*/0, kTestTimeEpoch -
GetFeedConfig().stale_content_threshold +
base::Minutes(1)),
base::DoNothing());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_FALSE(network_.query_request_sent);
// Verify the model is filled correctly.
EXPECT_STRINGS_EQUAL(ModelStateFor(MakeTypicalInitialModelState()),
stream_->GetModel(StreamType(StreamKind::kForYou))
->DumpStateForTesting());
}
TEST_F(FeedApiTest, LoadStreamFromStoreValidatesUser) {
// Fill the store with stream data for another user.
{
auto state = MakeTypicalInitialModelState();
state->stream_data.set_email("other@gmail.com");
store_->OverwriteStream(StreamType(StreamKind::kForYou), std::move(state),
base::DoNothing());
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, LoadingSpinnerIsSentInitially) {
store_->OverwriteStream(StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(), base::DoNothing());
TestForYouSurface surface(stream_.get());
ASSERT_EQ("loading", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, DetachSurfaceWhileLoadingModel) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
surface.Detach();
WaitForIdleTaskQueue();
EXPECT_EQ("loading", surface.DescribeUpdates());
EXPECT_TRUE(network_.query_request_sent);
}
TEST_F(FeedApiTest, AttachMultipleSurfacesLoadsModelOnce) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
TestForYouSurface other_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 2 slices", other_surface.DescribeUpdates());
// After load, another surface doesn't trigger any tasks,
// and immediately has content.
TestForYouSurface later_surface(stream_.get());
ASSERT_EQ("[user@foo] 2 slices", later_surface.DescribeUpdates());
EXPECT_TRUE(IsTaskQueueIdle());
}
TEST_P(FeedStreamTestForAllStreamTypes, ModelChangesAreSavedToStorage) {
store_->OverwriteStream(GetStreamType(), 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(GetStreamType(), operations);
WaitForIdleTaskQueue();
// Verify changes are applied to storage.
EXPECT_STRINGS_EQUAL(
ModelStateFor(MakeTypicalInitialModelState(), operations),
ModelStateFor(GetStreamType(), store_.get()));
// Unload and reload the model from the store, and verify we can still apply
// operations correctly.
surface.Detach();
surface.Clear();
UnloadModel(GetStreamType());
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(surface.GetStreamType(), operations2);
WaitForIdleTaskQueue();
EXPECT_STRINGS_EQUAL(
ModelStateFor(MakeTypicalInitialModelState(), operations, operations2),
ModelStateFor(GetStreamType(), store_.get()));
}
TEST_F(FeedApiTest, ReportSliceViewedIdentifiesCorrectIndex) {
store_->OverwriteStream(StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(), base::DoNothing());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
EXPECT_EQ(1, metrics_reporter_->slice_viewed_index);
}
TEST_F(FeedApiTest, ReportSliceViewed_AddViewedContentHashes) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
const feedstore::Metadata::StreamMetadata* stream_metadata =
feedstore::FindMetadataForStream(stream_->GetMetadata(),
StreamType(StreamKind::kForYou));
EXPECT_EQ(1, stream_metadata->viewed_content_hashes().size());
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(0).slice().slice_id());
stream_metadata = feedstore::FindMetadataForStream(
stream_->GetMetadata(), StreamType(StreamKind::kForYou));
EXPECT_EQ(2, stream_metadata->viewed_content_hashes().size());
// Reporting the slice viewed before will not be counted again.
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
stream_metadata = feedstore::FindMetadataForStream(
stream_->GetMetadata(), StreamType(StreamKind::kForYou));
EXPECT_EQ(2, stream_metadata->viewed_content_hashes().size());
}
TEST_F(FeedApiTest, ReportOpenInNewTabAction) {
store_->OverwriteStream(StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(), base::DoNothing());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
base::UserActionTester user_actions;
stream_->ReportOpenAction(
GURL(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id(),
OpenActionType::kNewTab);
EXPECT_EQ(1, user_actions.GetActionCount(
"ContentSuggestions.Feed.CardAction.OpenInNewTab"));
}
TEST_F(FeedApiTest, ReportOpenInNewTabInGroupAction) {
store_->OverwriteStream(StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(), base::DoNothing());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
base::UserActionTester user_actions;
stream_->ReportOpenAction(
GURL(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id(),
OpenActionType::kNewTabInGroup);
EXPECT_EQ(1, user_actions.GetActionCount(
"ContentSuggestions.Feed.CardAction.OpenInNewTabInGroup"));
}
TEST_F(FeedApiTest, HasUnreadContentAfterLoadFromNetwork) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({false, true}), observer.calls);
}
TEST_F(FeedApiTest, HasUnreadContentInitially) {
// Prime the feed with new content.
{
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
}
// Reload FeedStream. Add an observer before initialization completes.
// After initialization, the observer will be informed about unread content.
CreateStream(/*wait_for_initialization*/ false);
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({true}), observer.calls);
}
TEST_F(FeedApiTest, NetworkFetchWithNoNewContentDoesNotProvideUnreadContent) {
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
// Load content from the network, and view it.
{
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
}
// Wait until the feed content is stale.
task_environment_.FastForwardBy(base::Hours(100));
// Load content from the network again. This time there is no new content.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({false, true, false}), observer.calls);
}
TEST_F(FeedApiTest, RemovedUnreadContentObserverDoesNotReceiveCalls) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
stream_->RemoveUnreadContentObserver(StreamType(StreamKind::kForYou),
&observer);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({false}), observer.calls);
}
TEST_F(FeedApiTest, DeletedUnreadContentObserverDoesNotCrash) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
{
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou),
&observer);
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
}
TEST_F(FeedApiTest, HasUnreadContentAfterLoadFromStore) {
store_->OverwriteStream(StreamType(StreamKind::kForYou),
MakeTypicalInitialModelState(), base::DoNothing());
TestForYouSurface surface(stream_.get());
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({true}), observer.calls);
}
TEST_F(FeedApiTest, FollowForcesRefreshWhileSurfaceAttached_NotWorking) {
// Load the web feed stream.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Follow a web feed.
network_.InjectResponse(SuccessfulFollowResponse("dogs"));
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
WebFeedPageInformation page_info =
MakeWebFeedPageInformation("http://dogs.com");
CallbackReceiver<WebFeedSubscriptions::FollowWebFeedResult> callback;
stream_->subscriptions().FollowWebFeed(
page_info, WebFeedChangeReason::WEB_PAGE_MENU, callback.Bind());
ASSERT_EQ(WebFeedSubscriptionRequestStatus::kSuccess,
callback.RunAndGetResult().request_status);
// Content should refresh, but this is not implemented yet.
// ASSERT_EQ("loading -> 3 slices", surface.DescribeUpdates());
ASSERT_EQ("", surface.DescribeUpdates());
}
// Verify that following a web feed triggers a refresh.
// Also verify the unread content observer events.
TEST_F(FeedApiTest, FollowForcesRefresh) {
TestUnreadContentObserver unread_observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kFollowing),
&unread_observer);
// Load the web feed stream and view it.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
// Detach the surface.
surface.Detach();
// Follow a web feed.
network_.InjectResponse(SuccessfulFollowResponse("dogs"));
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
WebFeedPageInformation page_info =
MakeWebFeedPageInformation("http://dogs.com");
CallbackReceiver<WebFeedSubscriptions::FollowWebFeedResult> callback;
stream_->subscriptions().FollowWebFeed(
page_info, WebFeedChangeReason::WEB_PAGE_MENU, callback.Bind());
ASSERT_EQ(WebFeedSubscriptionRequestStatus::kSuccess,
callback.RunAndGetResult().request_status);
// Wait for model to unload and reattach surface. New content is loaded.
WaitForModelToAutoUnload();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> 3 slices", surface.DescribeUpdates());
EXPECT_EQ(std::vector<bool>({false, true, false, true}),
unread_observer.calls);
}
TEST_F(FeedApiTest, ReportFeedViewedUpdatesObservers) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
task_environment_.RunUntilIdle();
EXPECT_EQ(std::vector<bool>({true, false}), observer.calls);
// Verify that the fact the stream was viewed persists.
CreateStream();
TestUnreadContentObserver observer2;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou),
&observer2);
TestForYouSurface surface2(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(std::vector<bool>({false}), observer2.calls);
}
TEST_P(FeedStreamTestForAllStreamTypes, LoadMoreAppendsContent) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
// Ensure metrics reporter was informed at the start of the operation.
EXPECT_EQ(surface.GetSurfaceId(), metrics_reporter_->load_more_surface_id);
WaitForIdleTaskQueue();
ASSERT_EQ(absl::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, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(absl::optional<bool>(true), callback.GetResult());
EXPECT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
// The root ID should not change for next-page content.
EXPECT_EQ(MakeRootEventId(),
surface.update->logging_parameters().root_event_id());
}
TEST_P(FeedStreamTestForAllStreamTypes, LoadMorePersistsData) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(absl::optional<bool>(true), callback.GetResult());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(GetStreamType())->DumpStateForTesting(),
ModelStateFor(GetStreamType(), store_.get()));
}
TEST_F(FeedApiTest, LoadMorePersistAndLoadMore) {
// Verify we can persist a LoadMore, and then do another LoadMore after
// reloading state.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Load page 2.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(absl::optional<bool>(true), callback.GetResult());
surface.Detach();
UnloadModel(StreamType(StreamKind::kForYou));
// Load page 3.
surface.Attach(stream_.get());
response_translator_.InjectResponse(MakeTypicalNextPageState(3));
WaitForIdleTaskQueue();
callback.Clear();
surface.Clear();
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(absl::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(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kForYou), store_.get()));
}
TEST_F(FeedApiTest, LoadMoreSendsTokens) {
{
auto metadata = stream_->GetMetadata();
metadata.set_consistency_token("token");
stream_->SetMetadata(metadata);
}
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ("2 slices +spinner -> 4 slices", surface.DescribeUpdates());
EXPECT_EQ(
"token",
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());
response_translator_.InjectResponse(MakeTypicalNextPageState(3));
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
EXPECT_EQ(
"token",
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(FeedApiTest, LoadMoreAbortsIfNoNextPageToken) {
{
std::unique_ptr<StreamModelUpdateRequest> initial_state =
MakeTypicalInitialModelState();
initial_state->stream_data.clear_next_page_token();
response_translator_.InjectResponse(std::move(initial_state));
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
// LoadMore fails, and does not make an additional request.
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(absl::nullopt, metrics_reporter_->load_more_surface_id)
<< "metrics reporter was informed about a load more operation which "
"didn't begin";
}
TEST_F(FeedApiTest, LoadMoreFail) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Don't inject another response, which results in a proto translation
// failure.
CallbackReceiver<bool> callback;
stream_->LoadMore(surface, callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
EXPECT_EQ("2 slices +spinner -> 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, LoadMoreWithClearAllInResponse) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 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, callback.Bind());
WaitForIdleTaskQueue();
ASSERT_EQ(absl::optional<bool>(true), callback.GetResult());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kForYou), 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(FeedApiTest, LoadMoreBeforeLoad) {
CallbackReceiver<bool> callback;
TestForYouSurface surface;
stream_->LoadMore(surface, callback.Bind());
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
}
TEST_F(FeedApiTest, ReadNetworkResponse) {
base::HistogramTester histograms;
network_.InjectRealFeedQueryResponse();
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 10 slices", surface.DescribeUpdates());
// Verify we're processing some of the data on the request.
// The response has a privacy_notice_fulfilled=true.
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ActivityLoggingEnabled", 1, 1);
// A request schedule with two entries was in the response. The first entry
// should have already been scheduled/consumed, leaving only the second
// entry still in the the refresh_offsets vector.
RequestSchedule schedule = prefs::GetRequestSchedule(
RefreshTaskId::kRefreshForYouFeed, profile_prefs_);
EXPECT_EQ(std::vector<base::TimeDelta>({
base::Seconds(86308) + base::Nanoseconds(822963644),
base::Seconds(120000),
}),
schedule.refresh_offsets);
// The stream's user attributes are set, so activity logging is enabled.
EXPECT_TRUE(surface.update->logging_parameters().logging_enabled());
// This network response has content.
EXPECT_TRUE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
}
TEST_F(FeedApiTest, ReadNetworkResponseWithNoContent) {
base::HistogramTester histograms;
network_.InjectRealFeedQueryResponseWithNoContent();
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> loading -> no-cards", surface.DescribeUpdates());
// This network response has no content.
EXPECT_FALSE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
}
TEST_F(FeedApiTest, ClearAllAfterLoadResultsInRefresh) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->OnCacheDataCleared(); // triggers ClearAll().
response_translator_.InjectResponse(MakeTypicalInitialModelState());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices -> loading -> 2 slices",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, ClearAllWithNoSurfacesAttachedDoesNotReload) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, ClearAllWhileLoadingMoreDoesNotLoadMore) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
CallbackReceiver<bool> cr;
stream_->LoadMore(surface, cr.Bind());
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
EXPECT_EQ(false, cr.GetResult());
EXPECT_EQ(
"loading -> [user@foo] 2 slices -> 2 slices +spinner -> 2 slices -> "
"loading -> 2 slices",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, ClearAllWipesAllState) {
// Trigger saving a consistency token, so it can be cleared later.
network_.consistency_token = "token-11";
stream_->UploadAction(MakeFeedAction(42ul), CreateLoggingParameters(), true,
base::DoNothing());
// Trigger saving a feed stream, so it can be cleared later.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Enqueue an action, so it can be cleared later.
stream_->UploadAction(MakeFeedAction(43ul), CreateLoggingParameters(), false,
base::DoNothing());
// Trigger ClearAll, this should erase everything.
stream_->OnCacheDataCleared();
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices -> loading -> cant-refresh",
surface.DescribeUpdates());
EXPECT_EQ(R"("m": {
}
"recommendedIndex": {
}
"subs": {
}
)",
DumpStoreState(true));
EXPECT_EQ("", stream_->GetMetadata().consistency_token());
EXPECT_FALSE(surface.update->logging_parameters().logging_enabled());
}
TEST_F(FeedApiTest, StorePendingAction) {
stream_->UploadAction(MakeFeedAction(42ul), CreateLoggingParameters(), false,
base::DoNothing());
WaitForIdleTaskQueue();
std::vector<feedstore::StoredAction> result =
ReadStoredActions(stream_->GetStore());
ASSERT_EQ(1ul, result.size());
EXPECT_EQ(ToTextProto(MakeFeedAction(42ul).action_payload()),
ToTextProto(result[0].action().action_payload()));
}
TEST_F(FeedApiTest, UploadActionWhileSignedOutIsNoOp) {
account_info_ = {};
ASSERT_EQ(stream_->GetAccountInfo(), AccountInfo{});
stream_->UploadAction(MakeFeedAction(42ul), CreateLoggingParameters(), false,
base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(0ul, ReadStoredActions(stream_->GetStore()).size());
}
TEST_F(FeedApiTest, SignOutWhileUploadActionDoesNotUpload) {
stream_->UploadAction(MakeFeedAction(42ul), CreateLoggingParameters(), true,
base::DoNothing());
account_info_ = {};
WaitForIdleTaskQueue();
EXPECT_EQ(UploadActionsStatus::kAbortUploadForSignedOutUser,
metrics_reporter_->upload_action_status);
EXPECT_EQ(0, network_.GetActionRequestCount());
}
TEST_F(FeedApiTest, ClearAllWhileUploadActionDoesNotUpload) {
CallbackReceiver<UploadActionsTask::Result> cr;
stream_->UploadAction(MakeFeedAction(42ul), CreateLoggingParameters(), true,
cr.Bind());
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
EXPECT_EQ(UploadActionsStatus::kAbortUploadActionsWithPendingClearAll,
metrics_reporter_->upload_action_status);
EXPECT_EQ(0, network_.GetActionRequestCount());
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(0ul, cr.GetResult()->upload_attempt_count);
}
TEST_F(FeedApiTest, WrongUserUploadActionDoesNotUpload) {
CallbackReceiver<UploadActionsTask::Result> cr;
LoggingParameters logging_parameters = CreateLoggingParameters();
logging_parameters.email = "someothergaia";
stream_->UploadAction(MakeFeedAction(42ul), logging_parameters, true,
cr.Bind());
WaitForIdleTaskQueue();
// Action should not upload.
EXPECT_EQ(UploadActionsStatus::kAbortUploadForWrongUser,
metrics_reporter_->upload_action_status);
EXPECT_EQ(0, network_.GetActionRequestCount());
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(0ul, cr.GetResult()->upload_attempt_count);
}
TEST_F(FeedApiTest, LoggingPropertiesWithNoAccountDoesNotUpload) {
CallbackReceiver<UploadActionsTask::Result> cr;
LoggingParameters logging_parameters = CreateLoggingParameters();
logging_parameters.email.clear();
stream_->UploadAction(MakeFeedAction(42ul), logging_parameters, true,
cr.Bind());
WaitForIdleTaskQueue();
// Action should not upload.
EXPECT_EQ(UploadActionsStatus::kAbortUploadForSignedOutUser,
metrics_reporter_->upload_action_status);
EXPECT_EQ(0, network_.GetActionRequestCount());
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(0ul, cr.GetResult()->upload_attempt_count);
}
TEST_F(FeedApiTest, 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(),
CreateLoggingParameters());
}
WaitForIdleTaskQueue();
// Verify the action was uploaded.
EXPECT_EQ(1, network_.GetActionRequestCount());
std::vector<feedstore::StoredAction> result =
ReadStoredActions(stream_->GetStore());
ASSERT_EQ(0ul, result.size());
}
TEST_F(FeedApiTest, ProcessViewActionResultsInDelayedUpload) {
network_.consistency_token = "token-11";
stream_->ProcessViewAction(MakeFeedAction(42ul).SerializeAsString(),
CreateLoggingParameters());
WaitForIdleTaskQueue();
// Verify it's not uploaded immediately.
ASSERT_EQ(0, network_.GetActionRequestCount());
// Trigger a network refresh.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Verify the action was uploaded.
EXPECT_EQ(1, network_.GetActionRequestCount());
}
TEST_F(FeedApiTest, ProcessViewActionDroppedBecauseNotEnabled) {
network_.consistency_token = "token-11";
LoggingParameters logging_parameters = CreateLoggingParameters();
logging_parameters.view_actions_enabled = false;
stream_->ProcessViewAction(MakeFeedAction(42ul).SerializeAsString(),
logging_parameters);
WaitForIdleTaskQueue();
// Verify it's not uploaded, and not stored.
ASSERT_EQ(0, network_.GetActionRequestCount());
ASSERT_EQ(0ull, ReadStoredActions(stream_->GetStore()).size());
}
TEST_F(FeedApiTest, ActionsUploadWithoutConditionsWhenFeatureDisabled) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ProcessViewAction(
feedwire::FeedAction::default_instance().SerializeAsString(),
surface.GetLoggingParameters());
WaitForIdleTaskQueue();
stream_->ProcessThereAndBackAgain(
MakeThereAndBackAgainData(42ul).SerializeAsString(),
surface.GetLoggingParameters());
WaitForIdleTaskQueue();
// Verify the actions were uploaded.
ASSERT_EQ(1, network_.GetActionRequestCount());
EXPECT_EQ(2, network_.GetActionRequestSent()->feed_actions_size());
}
TEST_F(FeedApiTest, LoadStreamFromNetworkUploadsActions) {
stream_->UploadAction(MakeFeedAction(99ul), CreateLoggingParameters(), false,
base::DoNothing());
WaitForIdleTaskQueue();
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestCount());
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
// Uploaded action should have been erased from store.
stream_->UploadAction(MakeFeedAction(100ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(2, network_.GetActionRequestCount());
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
}
TEST_F(FeedApiTest, UploadedActionsHaveSequentialNumbers) {
// Send 3 actions.
stream_->UploadAction(MakeFeedAction(1ul), CreateLoggingParameters(), false,
base::DoNothing());
stream_->UploadAction(MakeFeedAction(2ul), CreateLoggingParameters(), false,
base::DoNothing());
stream_->UploadAction(MakeFeedAction(3ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.GetActionRequestCount());
feedwire::UploadActionsRequest request1 = *network_.GetActionRequestSent();
// Send another action in a new request.
stream_->UploadAction(MakeFeedAction(4ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.GetActionRequestCount());
feedwire::UploadActionsRequest request2 = *network_.GetActionRequestSent();
// Verify that sent actions have sequential numbers.
ASSERT_EQ(3, request1.feed_actions_size());
ASSERT_EQ(1, request2.feed_actions_size());
EXPECT_EQ(1, request1.feed_actions(0).client_data().sequence_number());
EXPECT_EQ(2, request1.feed_actions(1).client_data().sequence_number());
EXPECT_EQ(3, request1.feed_actions(2).client_data().sequence_number());
EXPECT_EQ(4, request2.feed_actions(0).client_data().sequence_number());
}
TEST_F(FeedApiTest, LoadMoreUploadsActions) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->UploadAction(MakeFeedAction(99ul), CreateLoggingParameters(), false,
base::DoNothing());
WaitForIdleTaskQueue();
network_.consistency_token = "token-12";
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
EXPECT_EQ("token-12", stream_->GetMetadata().consistency_token());
// Uploaded action should have been erased from the store.
network_.ClearTestData();
stream_->UploadAction(MakeFeedAction(100ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
EXPECT_EQ(
ToTextProto(MakeFeedAction(100ul).action_payload()),
ToTextProto(
network_.GetActionRequestSent()->feed_actions(0).action_payload()));
}
TEST_F(FeedApiTest, LoadMoreDoesNotUpdateLoggingEnabled) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_TRUE(surface.update->logging_parameters().logging_enabled());
int page = 2;
// A NextPage request will not work when signed-out.
const bool signed_in = true;
// Logging parameters are not updated on LoadMore(), so logging remains
// enabled until the next refresh.
for (bool waa_on : {true, false}) {
for (bool privacy_notice_fulfilled : {true, false}) {
response_translator_.InjectResponse(MakeTypicalNextPageState(
page++, kTestTimeEpoch, signed_in, waa_on, privacy_notice_fulfilled));
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_TRUE(surface.update->logging_parameters().logging_enabled());
}
}
}
TEST_F(FeedApiTest, LoadStreamWithLoggingEnabled) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_TRUE(surface.update->logging_parameters().logging_enabled());
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, BackgroundingAppUploadsActions) {
stream_->UploadAction(MakeFeedAction(1ul), CreateLoggingParameters(), false,
base::DoNothing());
stream_->OnEnterBackground();
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
EXPECT_EQ(
ToTextProto(MakeFeedAction(1ul).action_payload()),
ToTextProto(
network_.GetActionRequestSent()->feed_actions(0).action_payload()));
}
TEST_F(FeedApiTest, BackgroundingAppDoesNotUploadActions) {
Config config;
config.upload_actions_on_enter_background = false;
SetFeedConfigForTesting(config);
stream_->UploadAction(MakeFeedAction(1ul), CreateLoggingParameters(), false,
base::DoNothing());
stream_->OnEnterBackground();
WaitForIdleTaskQueue();
EXPECT_EQ(0, network_.GetActionRequestCount());
}
TEST_F(FeedApiTest, UploadedActionsAreNotSentAgain) {
stream_->UploadAction(MakeFeedAction(1ul), CreateLoggingParameters(), false,
base::DoNothing());
stream_->OnEnterBackground();
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.GetActionRequestCount());
stream_->OnEnterBackground();
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestCount());
}
TEST_F(FeedApiTest, UploadActionsOneBatch) {
UploadActions(
{MakeFeedAction(97ul), MakeFeedAction(98ul), MakeFeedAction(99ul)});
WaitForIdleTaskQueue();
EXPECT_EQ(1, network_.GetActionRequestCount());
EXPECT_EQ(3, network_.GetActionRequestSent()->feed_actions_size());
stream_->UploadAction(MakeFeedAction(99ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(2, network_.GetActionRequestCount());
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
}
TEST_F(FeedApiTest, 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_.GetActionRequestCount());
stream_->UploadAction(MakeFeedAction(99ul), CreateLoggingParameters(), true,
base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ(4, network_.GetActionRequestCount());
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
}
TEST_F(FeedApiTest, UploadActionsSkipsStaleActionsByTimestamp) {
stream_->UploadAction(MakeFeedAction(2ul), CreateLoggingParameters(), false,
base::DoNothing());
WaitForIdleTaskQueue();
task_environment_.FastForwardBy(base::Hours(25));
// Trigger upload
CallbackReceiver<UploadActionsTask::Result> cr;
stream_->UploadAction(MakeFeedAction(3ul), CreateLoggingParameters(), true,
cr.Bind());
WaitForIdleTaskQueue();
// Just one action should have been uploaded.
EXPECT_EQ(1, network_.GetActionRequestCount());
EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
EXPECT_EQ(
ToTextProto(MakeFeedAction(3ul).action_payload()),
ToTextProto(
network_.GetActionRequestSent()->feed_actions(0).action_payload()));
ASSERT_TRUE(cr.GetResult());
EXPECT_EQ(1ul, cr.GetResult()->upload_attempt_count);
EXPECT_EQ(1ul, cr.GetResult()->stale_count);
}
TEST_F(FeedApiTest, UploadActionsErasesStaleActionsByAttempts) {
// Three failed uploads, plus one more to cause the first action to be erased.
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(0ul), CreateLoggingParameters(), true,
base::DoNothing());
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(1ul), CreateLoggingParameters(), true,
base::DoNothing());
network_.InjectEmptyActionRequestResult();
stream_->UploadAction(MakeFeedAction(2ul), CreateLoggingParameters(), true,
base::DoNothing());
CallbackReceiver<UploadActionsTask::Result> cr;
stream_->UploadAction(MakeFeedAction(3ul), CreateLoggingParameters(), true,
cr.Bind());
WaitForIdleTaskQueue();
// Four requests, three pending actions in the last request.
EXPECT_EQ(4, network_.GetActionRequestCount());
EXPECT_EQ(3, network_.GetActionRequestSent()->feed_actions_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(FeedApiTest, MetadataLoadedWhenDatabaseInitialized) {
const auto kExpiry = kTestTimeEpoch + base::Days(1234);
{
// Write some metadata so it can be loaded when FeedStream starts up.
feedstore::Metadata initial_metadata;
feedstore::SetSessionId(initial_metadata, "session-id", kExpiry);
initial_metadata.set_consistency_token("token");
initial_metadata.set_gaia(GetAccountInfo().gaia);
store_->WriteMetadata(initial_metadata, base::DoNothing());
}
// Creating a stream should load metadata.
CreateStream();
EXPECT_EQ("session-id", stream_->GetMetadata().session_id().token());
EXPECT_TIME_EQ(kExpiry,
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
EXPECT_EQ("token", stream_->GetMetadata().consistency_token());
// Verify the schema has been updated to the current version.
EXPECT_EQ((int)FeedStore::kCurrentStreamSchemaVersion,
stream_->GetMetadata().stream_schema_version());
}
TEST_F(FeedApiTest, ClearAllWhenDatabaseInitializedForWrongUser) {
{
// Write some metadata so it can be loaded when FeedStream starts up.
feedstore::Metadata initial_metadata;
initial_metadata.set_consistency_token("token");
initial_metadata.set_gaia("someotherusergaia");
store_->WriteMetadata(initial_metadata, base::DoNothing());
}
// Creating a stream should init database.
CreateStream();
EXPECT_EQ("{\n}\n", DumpStoreState());
EXPECT_EQ("", stream_->GetMetadata().consistency_token());
}
TEST_F(FeedApiTest, ModelUnloadsAfterTimeout) {
Config config;
config.model_unload_timeout = base::Seconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::Milliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(surface.GetStreamType()));
task_environment_.FastForwardBy(base::Milliseconds(2));
WaitForIdleTaskQueue();
EXPECT_FALSE(stream_->GetModel(surface.GetStreamType()));
}
TEST_F(FeedApiTest, ModelDoesNotUnloadIfSurfaceIsAttached) {
Config config;
config.model_unload_timeout = base::Seconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::Milliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(surface.GetStreamType()));
surface.Attach(stream_.get());
task_environment_.FastForwardBy(base::Milliseconds(2));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(surface.GetStreamType()));
}
TEST_F(FeedApiTest, ModelUnloadsAfterSecondTimeout) {
Config config;
config.model_unload_timeout = base::Seconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::Milliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(surface.GetStreamType()));
// Attaching another surface will prolong the unload time for another second.
surface.Attach(stream_.get());
surface.Detach();
task_environment_.FastForwardBy(base::Milliseconds(999));
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(surface.GetStreamType()));
task_environment_.FastForwardBy(base::Milliseconds(2));
WaitForIdleTaskQueue();
EXPECT_FALSE(stream_->GetModel(surface.GetStreamType()));
}
TEST_F(FeedApiTest, SendsClientInstanceId) {
// Store is empty, so we should fallback to a network request.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
ASSERT_TRUE(network_.query_request_sent);
// Instance ID is a random token. Verify it is not empty.
std::string first_instance_id = network_.query_request_sent->feed_request()
.client_info()
.client_instance_id();
EXPECT_NE("", first_instance_id);
// No signed-out session id was in the request.
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id()
.empty());
// LoadMore, and verify the same token is used.
response_translator_.InjectResponse(MakeTypicalNextPageState(2));
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
EXPECT_EQ(first_instance_id, network_.query_request_sent->feed_request()
.client_info()
.client_instance_id());
// No signed-out session id was in the request.
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id()
.empty());
// Trigger a ClearAll to verify the instance ID changes.
stream_->OnSignedOut();
WaitForIdleTaskQueue();
EXPECT_FALSE(stream_->GetModel(surface.GetStreamType()));
const bool is_for_next_page = false; // No model so no first page yet.
const std::string new_instance_id =
stream_
->GetRequestMetadata(StreamType(StreamKind::kForYou),
is_for_next_page)
.client_instance_id;
ASSERT_NE("", new_instance_id);
ASSERT_NE(first_instance_id, new_instance_id);
}
TEST_F(FeedApiTest, SignedOutSessionIdConsistency) {
const std::string kSessionToken1("session-token-1");
const std::string kSessionToken2("session-token-2");
account_info_ = {};
StreamModelUpdateRequestGenerator model_generator;
model_generator.signed_in = false;
// (1) Do an initial load of the store
// - this should trigger a network request
// - the request should not include client-instance-id
// - the request should not include a session-id
// - the stream should capture the session-id token from the response
response_translator_.InjectResponse(model_generator.MakeFirstPage(),
kSessionToken1);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.client_instance_id()
.empty());
EXPECT_FALSE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.has_session_id());
EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
const base::Time kSessionToken1ExpiryTime =
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata());
// (2) LoadMore: the server returns the same session-id token
// - this should trigger a network request
// - the request should not include client-instance-id
// - the request should include the first session-id
// - the stream should retain the first session-id
// - the session-id's expiry time should be unchanged
task_environment_.FastForwardBy(base::Seconds(1));
response_translator_.InjectResponse(model_generator.MakeNextPage(2),
kSessionToken1);
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.client_instance_id()
.empty());
EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id());
EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
EXPECT_TIME_EQ(kSessionToken1ExpiryTime,
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
// (3) LoadMore: the server omits returning a session-id token
// - this should trigger a network request
// - the request should not include client-instance-id
// - the request should include the first session-id
// - the stream should retain the first session-id
// - the session-id's expiry time should be unchanged
task_environment_.FastForwardBy(base::Seconds(1));
response_translator_.InjectResponse(model_generator.MakeNextPage(3));
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(3, network_.send_query_call_count);
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.client_instance_id()
.empty());
EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id());
EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
EXPECT_TIME_EQ(kSessionToken1ExpiryTime,
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
// (4) LoadMore: the server returns new session id.
// - this should trigger a network request
// - the request should not include client-instance-id
// - the request should include the first session-id
// - the stream should retain the second session-id
// - the new session-id's expiry time should be updated
task_environment_.FastForwardBy(base::Seconds(1));
response_translator_.InjectResponse(model_generator.MakeNextPage(4),
kSessionToken2);
stream_->LoadMore(surface, base::DoNothing());
WaitForIdleTaskQueue();
ASSERT_EQ(4, network_.send_query_call_count);
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.client_instance_id()
.empty());
EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id());
EXPECT_EQ(kSessionToken2, stream_->GetMetadata().session_id().token());
EXPECT_TIME_EQ(kSessionToken1ExpiryTime + base::Seconds(3),
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
}
TEST_F(FeedApiTest, ClearAllResetsSessionId) {
account_info_ = {};
// Initialize a session id.
feedstore::Metadata metadata = stream_->GetMetadata();
feedstore::MaybeUpdateSessionId(metadata, "session-id");
stream_->SetMetadata(metadata);
// Trigger a ClearAll.
stream_->OnCacheDataCleared();
WaitForIdleTaskQueue();
// Session-ID should be wiped.
EXPECT_TRUE(stream_->GetMetadata().session_id().token().empty());
EXPECT_TRUE(
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()).is_null());
}
TEST_F(FeedApiTest, SignedOutSessionIdExpiry) {
const std::string kSessionToken1("session-token-1");
const std::string kSessionToken2("session-token-2");
account_info_ = {};
StreamModelUpdateRequestGenerator model_generator;
model_generator.signed_in = false;
// (1) Do an initial load of the store
// - this should trigger a network request
// - the request should not include a session-id
// - the stream should capture the session-id token from the response
response_translator_.InjectResponse(model_generator.MakeFirstPage(),
kSessionToken1);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_FALSE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.has_session_id());
EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
// (2) Reload the stream from the network:
// - Detach the surface, advance the clock beyond the stale content
// threshold, re-attach the surface.
// - this should trigger a network request
// - the request should include kSessionToken1
// - the stream should retain the original session-id
surface.Detach();
task_environment_.FastForwardBy(GetFeedConfig().stale_content_threshold +
base::Seconds(1));
response_translator_.InjectResponse(model_generator.MakeFirstPage());
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.session_id());
EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
// (3) Reload the stream from the network:
// - Detach the surface, advance the clock beyond the session id max age
// threshold, re-attach the surface.
// - this should trigger a network request
// - the request should not include a session-id
// - the stream should get a new session-id
surface.Detach();
task_environment_.FastForwardBy(GetFeedConfig().session_id_max_age -
GetFeedConfig().stale_content_threshold);
ASSERT_LT(feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()),
base::Time::Now());
response_translator_.InjectResponse(model_generator.MakeFirstPage(),
kSessionToken2);
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(3, network_.send_query_call_count);
EXPECT_FALSE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.has_session_id());
EXPECT_EQ(kSessionToken2, stream_->GetMetadata().session_id().token());
}
TEST_F(FeedApiTest, SessionIdPersistsAcrossStreamLoads) {
const std::string kSessionToken("session-token-ftw");
const base::Time kExpiryTime =
kTestTimeEpoch + GetFeedConfig().session_id_max_age;
StreamModelUpdateRequestGenerator model_generator;
model_generator.signed_in = false;
account_info_ = {};
// (1) Do an initial load of the store
// - this should trigger a network request
// - the stream should capture the session-id token from the response
response_translator_.InjectResponse(model_generator.MakeFirstPage(),
kSessionToken);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
// (2) Reload the metadata from disk.
// - the network query call count should be unchanged
// - the session token and expiry time should have been reloaded.
surface.Detach();
CreateStream();
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_EQ(kSessionToken, stream_->GetMetadata().session_id().token());
EXPECT_TIME_EQ(kExpiryTime,
feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
}
TEST_F(FeedApiTest, PersistentKeyValueStoreIsClearedOnClearAll) {
// Store some data and verify it exists.
PersistentKeyValueStore& store = stream_->GetPersistentKeyValueStore();
store.Put("x", "y", base::DoNothing());
CallbackReceiver<PersistentKeyValueStore::Result> get_result;
store.Get("x", get_result.Bind());
ASSERT_EQ("y", *get_result.RunAndGetResult().get_result);
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
// Verify ClearAll() deleted the data.
get_result.Clear();
store.Get("x", get_result.Bind());
EXPECT_FALSE(get_result.RunAndGetResult().get_result);
}
TEST_F(FeedApiTest, LoadMultipleStreams) {
// TODO(crbug.com/1369777) Add support for single web feed.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices",
for_you_surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 2 slices",
web_feed_surface.DescribeUpdates());
}
TEST_F(FeedApiTest, UnloadOnlyOneOfMultipleModels) {
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
Config config;
config.model_unload_timeout = base::Seconds(1);
SetFeedConfigForTesting(config);
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
for_you_surface.Detach();
WaitForModelToAutoUnload();
WaitForIdleTaskQueue();
EXPECT_TRUE(stream_->GetModel(StreamType(StreamKind::kFollowing)));
EXPECT_FALSE(stream_->GetModel(StreamType(StreamKind::kForYou)));
}
TEST_F(FeedApiTest, ExperimentsAreClearedOnClearAll) {
Experiments e;
std::vector<std::string> group_list1{"Group1"};
std::vector<std::string> group_list2{"Group2"};
e["Trial1"] = group_list1;
e["Trial2"] = group_list2;
prefs::SetExperiments(e, profile_prefs_);
stream_->OnCacheDataCleared(); // triggers ClearAll().
WaitForIdleTaskQueue();
Experiments empty;
Experiments got = prefs::GetExperiments(profile_prefs_);
EXPECT_EQ(got, empty);
}
TEST_F(FeedApiTest, CreateAndCommitEphemeralChange) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EphemeralChangeId change_id = stream_->CreateEphemeralChange(
surface.GetStreamType(), {MakeOperation(MakeClearAll())});
stream_->CommitEphemeralChange(surface.GetStreamType(), change_id);
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices -> no-cards -> no-cards",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, CreateAndCommitEphemeralChangeOnNoOperation) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EphemeralChangeId change_id =
stream_->CreateEphemeralChange(surface.GetStreamType(), {});
stream_->CommitEphemeralChange(surface.GetStreamType(), change_id);
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices -> 2 slices -> 2 slices",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, RejectEphemeralChange) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EphemeralChangeId change_id = stream_->CreateEphemeralChange(
surface.GetStreamType(), {MakeOperation(MakeClearAll())});
stream_->RejectEphemeralChange(surface.GetStreamType(), change_id);
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices -> no-cards -> 2 slices",
surface.DescribeUpdates());
}
// Test that we overwrite stored stream data, even if ContentId's do not change.
TEST_F(FeedApiTest, StreamDataOverwritesOldStream) {
// Inject two FeedQuery responses with some different data.
{
std::unique_ptr<StreamModelUpdateRequest> new_state =
MakeTypicalInitialModelState();
new_state->shared_states[0].set_shared_state_data("new-shared-data");
new_state->content[0].set_frame("new-frame-data");
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(std::move(new_state));
}
// Trigger stream load, unload stream, and wait until the stream data is
// stale.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
task_environment_.FastForwardBy(base::Days(20));
// Trigger stream load again, it should refersh from the network.
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
// Verify the new data was stored.
std::unique_ptr<StreamModelUpdateRequest> stored_data =
StoredModelData(StreamType(StreamKind::kForYou), store_.get());
ASSERT_TRUE(stored_data);
EXPECT_EQ("new-shared-data",
stored_data->shared_states[0].shared_state_data());
EXPECT_EQ("new-frame-data", stored_data->content[0].frame());
}
TEST_F(FeedApiTest, HasUnreadContentIsFalseAfterFeedViewed) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
ASSERT_TRUE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
EXPECT_FALSE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
}
TEST_F(FeedApiTest, HasUnreadContentRemainsFalseIfFeedViewedBeforeRefresh) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_TRUE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices -> 3 slices",
surface.DescribeUpdates());
EXPECT_FALSE(stream_->HasUnreadContent(StreamType(StreamKind::kForYou)));
}
TEST_F(FeedApiTest,
LoadingForYouStreamTriggersWebFeedRefreshIfNoUnreadContent) {
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
// Both streams should be fetched.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
// Attach a Web Feed surface, and verify content is loaded from the store.
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices",
web_feed_surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromStore,
metrics_reporter_->load_stream_status);
}
TEST_F(FeedApiTest,
LoadingForYouStreamDoesNotTriggerWebFeedRefreshIfNoSubscriptions) {
// Only for-you feed is fetched on load.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(1, network_.send_query_call_count);
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
EXPECT_EQ(LoadStreamStatus::kNotAWebFeedSubscriber,
metrics_reporter_->Stream(StreamType(StreamKind::kFollowing))
.background_refresh_status);
}
TEST_F(
FeedApiTest,
LoadForYouStreamDoesNotTriggerWebFeedRefreshContentIfIsAlreadyAvailable) {
// WebFeed stream is only fetched when there's a subscription.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
// Both streams should be fetched because there is no unread web-feed content.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ(2, network_.send_query_call_count);
// Detach and re-attach the surface. The for-you feed should be loaded again,
// but this time the web-feed is not refreshed.
surface.Detach();
UnloadModel(surface.GetStreamType());
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
// Neither stream type should be refreshed.
ASSERT_EQ(2, network_.send_query_call_count);
}
TEST_F(FeedApiTest, WasUrlRecentlyNavigatedFromFeed) {
const GURL url1("https://someurl1");
const GURL url2("https://someurl2");
EXPECT_FALSE(stream_->WasUrlRecentlyNavigatedFromFeed(url1));
EXPECT_FALSE(stream_->WasUrlRecentlyNavigatedFromFeed(url2));
stream_->ReportOpenAction(url1, StreamType(StreamKind::kForYou), "slice",
OpenActionType::kDefault);
stream_->ReportOpenAction(url2, StreamType(StreamKind::kForYou), "slice",
OpenActionType::kNewTab);
EXPECT_TRUE(stream_->WasUrlRecentlyNavigatedFromFeed(url1));
EXPECT_TRUE(stream_->WasUrlRecentlyNavigatedFromFeed(url2));
}
// After 10 URLs are navigated, they are forgotten in FIFO order.
TEST_F(FeedApiTest, WasUrlRecentlyNavigatedFromFeedMaxHistory) {
std::vector<GURL> urls;
for (int i = 0; i < 11; ++i)
urls.emplace_back("https://someurl" + base::NumberToString(i));
for (const GURL& url : urls)
stream_->ReportOpenAction(url, StreamType(StreamKind::kForYou), "slice",
OpenActionType::kDefault);
EXPECT_FALSE(stream_->WasUrlRecentlyNavigatedFromFeed(urls[0]));
for (size_t i = 1; i < urls.size(); ++i) {
EXPECT_TRUE(stream_->WasUrlRecentlyNavigatedFromFeed(urls[i]));
}
}
TEST_F(FeedApiTest, ClearAllOnStartupIfFeedIsDisabled) {
CallbackReceiver<> on_clear_all;
on_clear_all_ = on_clear_all.BindRepeating();
// Fetch a feed, so that there's stored data.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Turn off the feed, and re-create FeedStream. It should perform a ClearAll.
profile_prefs_.SetBoolean(feed::prefs::kEnableSnippets, false);
CreateStream();
EXPECT_TRUE(on_clear_all.called());
// Re-create the feed, and verify ClearAll isn't called again.
on_clear_all.Clear();
base::HistogramTester histograms;
CreateStream();
EXPECT_FALSE(on_clear_all.called());
histograms.ExpectUniqueSample("ContentSuggestions.Feed.UserSettingsOnStart",
UserSettingsOnStart::kFeedNotEnabledByPolicy,
1);
}
TEST_F(FeedApiTest, ReportUserSettingsFromMetadataWaaOnDpOff) {
// Fetch a feed, so that there's stored data.
{
RefreshResponseData response;
response.model_update_request = MakeTypicalInitialModelState();
response.web_and_app_activity_enabled = true;
response.last_fetch_timestamp = base::Time::Now();
response_translator_.InjectResponse(std::move(response));
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Simulate a Chrome restart.
base::HistogramTester histograms;
CreateStream();
histograms.ExpectUniqueSample("ContentSuggestions.Feed.UserSettingsOnStart",
UserSettingsOnStart::kSignedInWaaOnDpOff, 1);
EXPECT_EQ(
std::vector<std::string>({"SignedInNoRecentData", "SignedInWaaOnDpOff"}),
register_feed_user_settings_field_trial_calls_);
}
TEST_F(FeedApiTest, ReportUserSettingsFromMetadataWaaOffDpOn) {
// Fetch a feed, so that there's stored data.
{
RefreshResponseData response;
response.model_update_request = MakeTypicalInitialModelState();
response.discover_personalization_enabled = true;
response.last_fetch_timestamp = base::Time::Now();
response_translator_.InjectResponse(std::move(response));
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Simulate a Chrome restart.
base::HistogramTester histograms;
CreateStream();
histograms.ExpectUniqueSample("ContentSuggestions.Feed.UserSettingsOnStart",
UserSettingsOnStart::kSignedInWaaOffDpOn, 1);
}
TEST_F(FeedStreamTestForAllStreamTypes, ManualRefreshWithoutSurfaceIsAborted) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
surface.Detach();
WaitForIdleTaskQueue();
surface.Clear();
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
// Refresh fails, and surface is not updated.
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
EXPECT_EQ("", surface.DescribeUpdates());
}
TEST_F(FeedStreamTestForAllStreamTypes, ManualRefreshInterestFeedSuccess) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface2(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface2.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(true), callback.GetResult());
EXPECT_EQ("3 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
// Check that the root event ID has been updated.
EXPECT_EQ(MakeTypicalRefreshModelState()->stream_data.root_event_id(),
surface.update->logging_parameters().root_event_id());
EXPECT_NE(MakeTypicalInitialModelState()->stream_data.root_event_id(),
surface.update->logging_parameters().root_event_id());
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kForYou), store_.get()));
// Verify another feed is not affected.
EXPECT_EQ("", surface2.DescribeUpdates());
}
TEST_F(FeedStreamTestForAllStreamTypes, ManualRefreshWebFeedSuccess) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface2(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface2.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(true), callback.GetResult());
EXPECT_EQ("3 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kFollowing), store_.get()));
// Verify another feed is not affected.
EXPECT_EQ("", surface2.DescribeUpdates());
}
TEST_F(FeedApiTest, ManualRefreshFailsBecauseNetworkRequestFails) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
std::string original_store_dump =
ModelStateFor(surface.GetStreamType(), store_.get());
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
original_store_dump);
// Since we didn't inject a network response, the network update will fail.
// The store should not be updated.
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
EXPECT_EQ("cant-refresh", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed,
metrics_reporter_->load_stream_status);
EXPECT_STRINGS_EQUAL(ModelStateFor(surface.GetStreamType(), store_.get()),
original_store_dump);
}
TEST_F(FeedApiTest, ManualRefreshSuccessAfterUnload) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
UnloadModel(surface.GetStreamType());
WaitForIdleTaskQueue();
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(true), callback.GetResult());
EXPECT_EQ("3 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kForYou), store_.get()));
}
TEST_F(FeedApiTest, ManualRefreshSuccessAfterPreviousLoadFailure) {
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> cant-refresh", surface.DescribeUpdates());
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
EXPECT_EQ(absl::optional<bool>(true), callback.GetResult());
EXPECT_EQ("no-cards -> [user@foo] 3 slices", surface.DescribeUpdates());
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->load_stream_status);
// Verify stored state is equivalent to in-memory model.
EXPECT_STRINGS_EQUAL(
stream_->GetModel(surface.GetStreamType())->DumpStateForTesting(),
ModelStateFor(StreamType(StreamKind::kForYou), store_.get()));
}
TEST_F(FeedApiTest, ManualRefreshFailesWhenLoadingInProgress) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
// Don't call WaitForIdleTaskQueue to finish the loading.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
// Manual refresh should fail immediately when loading is still in progress.
EXPECT_EQ(absl::optional<bool>(false), callback.GetResult());
// The initial loading should finish.
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, ManualRefresh_MetricsOnNoCardViewed) {
base::HistogramTester histograms;
// Load the initial page.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Manual refresh.
task_environment_.FastForwardBy(base::Seconds(100));
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->ManualRefresh(surface.GetStreamType(), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ManualRefreshInterval", base::Seconds(100), 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardCountAtManualRefresh", 0, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardPercentageAtManualRefresh", 0, 1);
}
TEST_F(FeedApiTest, ManualRefresh_MetricsOnCardsViewed) {
base::HistogramTester histograms;
// Load the initial page.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// View a card.
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
WaitForIdleTaskQueue();
// Manual refresh.
task_environment_.FastForwardBy(base::Seconds(100));
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->ManualRefresh(surface.GetStreamType(), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ManualRefreshInterval", base::Seconds(100), 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardCountAtManualRefresh", 1, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardPercentageAtManualRefresh", 50, 1);
// View a card.
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.update->updated_slices(0).slice().slice_id());
WaitForIdleTaskQueue();
// Manual refresh.
task_environment_.FastForwardBy(base::Seconds(200));
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->ManualRefresh(surface.GetStreamType(), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectBucketCount("ContentSuggestions.Feed.ManualRefreshInterval",
base::Seconds(100).InMilliseconds(), 1);
histograms.ExpectBucketCount("ContentSuggestions.Feed.ManualRefreshInterval",
base::Seconds(200).InMilliseconds(), 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardCountAtManualRefresh", 1, 2);
histograms.ExpectBucketCount(
"ContentSuggestions.Feed.ViewedCardPercentageAtManualRefresh", 50, 1);
histograms.ExpectBucketCount(
"ContentSuggestions.Feed.ViewedCardPercentageAtManualRefresh", 33, 1);
}
TEST_F(FeedApiTest, ManualRefresh_MetricsOnCardsViewedAfterRestart) {
base::HistogramTester histograms;
// Load the initial page.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// View a card.
stream_->ReportSliceViewed(
surface.GetSurfaceId(), surface.GetStreamType(),
surface.initial_state->updated_slices(1).slice().slice_id());
WaitForIdleTaskQueue();
histograms.ExpectUniqueSample("NewTabPage.ContentSuggestions.Shown", 1, 1);
// Simulate a Chrome restart.
CreateStream();
TestForYouSurface surface2(stream_.get());
WaitForIdleTaskQueue();
// View the same card.
stream_->ReportSliceViewed(
surface2.GetSurfaceId(), surface2.GetStreamType(),
surface2.initial_state->updated_slices(1).slice().slice_id());
WaitForIdleTaskQueue();
histograms.ExpectUniqueSample("NewTabPage.ContentSuggestions.Shown", 1, 2);
// Manual refresh.
task_environment_.FastForwardBy(base::Seconds(100));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ManualRefresh(surface.GetStreamType(), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueTimeSample(
"ContentSuggestions.Feed.ManualRefreshInterval", base::Seconds(100), 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardCountAtManualRefresh", 1, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ViewedCardPercentageAtManualRefresh", 50, 1);
}
TEST_F(FeedApiTest, StartSurface) {
CreateStream(/*wait_for_initialization=*/true, /*start_surface=*/true);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent.has_value());
EXPECT_TRUE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.start_surface());
}
TEST_F(FeedApiTest, NoStartSurface) {
CreateStream(/*wait_for_initialization=*/true, /*start_surface=*/false);
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
CallbackReceiver<bool> callback;
stream_->ManualRefresh(surface.GetStreamType(), callback.Bind());
WaitForIdleTaskQueue();
ASSERT_TRUE(network_.query_request_sent.has_value());
EXPECT_FALSE(network_.query_request_sent->feed_request()
.client_info()
.chrome_client_info()
.start_surface());
}
TEST_F(FeedApiTest, ForYouContentOrderUnset) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(
feedwire::FeedQuery::ContentOrder::
FeedQuery_ContentOrder_CONTENT_ORDER_UNSPECIFIED,
network_.query_request_sent->feed_request().feed_query().order_by());
}
TEST_F(FeedApiTest, ContentOrderIsGroupedByDefault) {
base::HistogramTester histograms;
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(
feedwire::FeedQuery::ContentOrder::FeedQuery_ContentOrder_GROUPED,
network_.query_request_sent->feed_request().feed_query().order_by());
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.WebFeed.RefreshContentOrder",
ContentOrder::kGrouped, 1);
}
TEST_F(FeedApiTest, SetContentOrderReloadsContent) {
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
base::HistogramTester histograms;
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->SetContentOrder(StreamType(StreamKind::kFollowing),
ContentOrder::kReverseChron);
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices -> loading -> 2 slices",
surface.DescribeUpdates());
EXPECT_EQ(
feedwire::FeedQuery::ContentOrder::FeedQuery_ContentOrder_RECENT,
network_.query_request_sent->feed_request().feed_query().order_by());
EXPECT_EQ(ContentOrder::kReverseChron,
stream_->GetContentOrder(StreamType(StreamKind::kFollowing)));
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.WebFeed.RefreshContentOrder",
ContentOrder::kReverseChron, 1);
}
TEST_F(FeedApiTest, SetContentOrderIsSavedeNotRefreshedIfUnchanged) {
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
// "Raw prefs" order value should start as unspecified.
EXPECT_EQ(ContentOrder::kUnspecified,
feed::prefs::GetWebFeedContentOrder(profile_prefs_));
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->SetContentOrder(StreamType(StreamKind::kFollowing),
ContentOrder::kGrouped);
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// "Raw prefs" order value should have been updated.
EXPECT_EQ(ContentOrder::kGrouped,
feed::prefs::GetWebFeedContentOrder(profile_prefs_));
EXPECT_EQ(ContentOrder::kGrouped,
stream_->GetContentOrder(StreamType(StreamKind::kFollowing)));
}
TEST_F(FeedApiTest, ContentOrderIsFinchControllable) {
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
base::test::ScopedFeatureList scoped_feature_list;
base::FieldTrialParams params;
params["following_feed_content_order"] = "reverse_chron";
scoped_feature_list.InitAndEnableFeatureWithParameters(kWebFeed, params);
CreateStream();
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(
feedwire::FeedQuery::ContentOrder::FeedQuery_ContentOrder_RECENT,
network_.query_request_sent->feed_request().feed_query().order_by());
EXPECT_EQ(ContentOrder::kReverseChron,
stream_->GetContentOrder(StreamType(StreamKind::kFollowing)));
}
TEST_F(FeedApiTest, ContentOrderPrefOverridesFinch) {
network_.InjectListWebFeedsResponse({MakeWireWebFeed("cats")});
base::test::ScopedFeatureList scoped_feature_list;
// Sets the "raw prefs" order value
feed::prefs::SetWebFeedContentOrder(profile_prefs_, ContentOrder::kGrouped);
base::FieldTrialParams params;
params["following_feed_content_order"] = "reverse_chron";
scoped_feature_list.InitAndEnableFeatureWithParameters(kWebFeed, params);
CreateStream();
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestWebFeedSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
EXPECT_EQ(
feedwire::FeedQuery::ContentOrder::FeedQuery_ContentOrder_GROUPED,
network_.query_request_sent->feed_request().feed_query().order_by());
EXPECT_EQ(ContentOrder::kGrouped,
stream_->GetContentOrder(StreamType(StreamKind::kFollowing)));
}
// This is a regression test for crbug.com/1249772.
TEST_F(FeedApiTest, SignInWhileSurfaceIsOpen) {
account_info_ = {}; // not signed in initially.
// Load content and simulate a restart, so that there is stored content.
{
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
CreateStream();
}
// Simulate signing-in while the feed is open.
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(surface.GetStreamType(), surface.GetSurfaceId());
TestUnreadContentObserver observer;
stream_->AddUnreadContentObserver(StreamType(StreamKind::kForYou), &observer);
account_info_ = TestAccountInfo();
stream_->OnSignedIn();
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
WaitForIdleTaskQueue();
EXPECT_EQ("loading -> 2 slices -> loading -> [user@foo] 3 slices",
surface.DescribeUpdates());
// Even though content is updated, the feed remains in view, so content is not
// unread.
EXPECT_EQ(std::vector<bool>({false}), observer.calls);
}
TEST_F(FeedApiTest, SignOutWhileSurfaceIsOpen) {
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
account_info_ = {};
stream_->OnSignedOut();
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
WaitForIdleTaskQueue();
EXPECT_EQ(
"loading -> [user@foo] 2 slices -> loading -> [NO Logging] 3 slices",
surface.DescribeUpdates());
}
TEST_F(FeedApiTest, FollowedFromWebPageMenuCount) {
// Arrange.
network_.InjectListWebFeedsResponse({MakeWireWebFeed("dogs")});
TestWebFeedSurface surface(stream_.get());
// Act.
stream_->IncrementFollowedFromWebPageMenuCount();
// Wait for the surface to load and a network request to be sent.
WaitForIdleTaskQueue();
// Assert.
EXPECT_EQ(1, stream_->GetMetadata().followed_from_web_page_menu_count());
EXPECT_EQ(
1, stream_->GetRequestMetadata(StreamType(StreamKind::kFollowing), false)
.followed_from_web_page_menu_count);
// We should report exactly 1 case of followed from the menu in the feed
// request.
ASSERT_EQ(1, network_.query_request_sent->feed_request()
.feed_query()
.chrome_fulfillment_info()
.chrome_feature_usage()
.times_followed_from_web_page_menu());
}
TEST_F(FeedApiTest, InfoCardTrackingActions) {
// Set up the server and client timestamps that affect the computation of
// the view timestamps in the info card tracking state.
base::Time server_timestamp = base::Time::Now();
task_environment_.AdvanceClock(base::Seconds(100));
base::Time client_timestamp = base::Time::Now();
base::TimeDelta timestamp_adjustment = server_timestamp - client_timestamp;
task_environment_.AdvanceClock(base::Seconds(200));
// Load the initial page.
RefreshResponseData response;
response.model_update_request = MakeTypicalInitialModelState();
response.last_fetch_timestamp = client_timestamp;
response.server_response_sent_timestamp = server_timestamp;
response_translator_.InjectResponse(std::move(response));
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
base::HistogramTester histograms;
// Perform actions on one info card and verify the histograms.
base::Time first_view_timestamp2 = base::Time::Now() + timestamp_adjustment;
base::Time last_view_timestamp2 = first_view_timestamp2;
stream_->ReportInfoCardTrackViewStarted(StreamType(StreamKind::kForYou),
kTestInfoCardType2);
stream_->ReportInfoCardViewed(StreamType(StreamKind::kForYou),
kTestInfoCardType2,
kMinimumViewIntervalSeconds);
stream_->ReportInfoCardClicked(StreamType(StreamKind::kForYou),
kTestInfoCardType2);
stream_->ReportInfoCardClicked(StreamType(StreamKind::kForYou),
kTestInfoCardType2);
histograms.ExpectUniqueSample("ContentSuggestions.Feed.InfoCard.Started",
kTestInfoCardType2, 1);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Viewed",
kTestInfoCardType2, 1);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Clicked",
kTestInfoCardType2, 2);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Dismissed",
kTestInfoCardType2, 0);
// Perform actions on another info card and verify the histograms.
base::Time first_view_timestamp1 = base::Time::Now() + timestamp_adjustment;
stream_->ReportInfoCardViewed(StreamType(StreamKind::kForYou),
kTestInfoCardType1,
kMinimumViewIntervalSeconds);
task_environment_.AdvanceClock(base::Seconds(kMinimumViewIntervalSeconds));
stream_->ReportInfoCardViewed(StreamType(StreamKind::kForYou),
kTestInfoCardType1,
kMinimumViewIntervalSeconds);
task_environment_.AdvanceClock(base::Seconds(kMinimumViewIntervalSeconds));
base::Time last_view_timestamp1 = base::Time::Now() + timestamp_adjustment;
stream_->ReportInfoCardViewed(StreamType(StreamKind::kForYou),
kTestInfoCardType1,
kMinimumViewIntervalSeconds);
stream_->ReportInfoCardClicked(StreamType(StreamKind::kForYou),
kTestInfoCardType1);
stream_->ReportInfoCardDismissedExplicitly(StreamType(StreamKind::kForYou),
kTestInfoCardType1);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Started",
kTestInfoCardType1, 0);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Viewed",
kTestInfoCardType1, 3);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Clicked",
kTestInfoCardType1, 1);
histograms.ExpectBucketCount("ContentSuggestions.Feed.InfoCard.Dismissed",
kTestInfoCardType1, 1);
// Refresh the page so that a feed query including the info card tracking
// states is sent. Call "CreateStream()" before the refresh to simulate
// Chrome restart. This is used to test that info card tracking states are
// sent in the initial page load when stream model is not loaded yet.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
surface.Detach();
CreateStream();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
WaitForIdleTaskQueue();
// Verify the info card tracking states. There should be 2 states with
// expected counts and view timestamps populated.
ASSERT_EQ(2, network_.query_request_sent->feed_request()
.feed_query()
.chrome_fulfillment_info()
.info_card_tracking_state_size());
feedwire::InfoCardTrackingState state1;
state1.set_type(kTestInfoCardType1);
state1.set_view_count(3);
state1.set_click_count(1);
state1.set_explicitly_dismissed_count(1);
state1.set_first_view_timestamp(
feedstore::ToTimestampMillis(first_view_timestamp1));
state1.set_last_view_timestamp(
feedstore::ToTimestampMillis(last_view_timestamp1));
EXPECT_THAT(state1, EqualsProto(network_.query_request_sent->feed_request()
.feed_query()
.chrome_fulfillment_info()
.info_card_tracking_state(0)));
feedwire::InfoCardTrackingState state2;
state2.set_type(kTestInfoCardType2);
state2.set_view_count(1);
state2.set_click_count(2);
state2.set_first_view_timestamp(
feedstore::ToTimestampMillis(first_view_timestamp2));
state2.set_last_view_timestamp(
feedstore::ToTimestampMillis(last_view_timestamp2));
EXPECT_THAT(state2, EqualsProto(network_.query_request_sent->feed_request()
.feed_query()
.chrome_fulfillment_info()
.info_card_tracking_state(1)));
}
TEST_F(FeedApiTest, InvalidateFeedCache_CauseRefreshOnNextLoad) {
// Load the ForYou feed with initial content from the network.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Invalidate cached content and reattach the surface, waiting for the model
// to unload. It should refresh from the network with refreshed content.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->InvalidateContentCacheFor(StreamKind::kForYou);
surface.Detach();
WaitForModelToAutoUnload();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_EQ("loading -> 3 slices", surface.DescribeUpdates());
}
TEST_F(FeedApiTest, InvalidateFeedCache_DoesNotForceRefreshFeedWhileInUse) {
// Load the ForYou feed with initial content from the network.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
ASSERT_EQ("loading -> [user@foo] 2 slices", surface.DescribeUpdates());
// Invalidate the ForYou feed and verify nothing changes right away.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->InvalidateContentCacheFor(StreamKind::kForYou);
WaitForIdleTaskQueue();
ASSERT_EQ("", surface.DescribeUpdates());
EXPECT_FALSE(response_translator_.InjectedResponseConsumed());
}
TEST_F(FeedApiTest, InvalidateFeedCache_UnknownDoesNotForceRefreshAnyFeeds) {
// Enable WebFeed and WebFeedOnboarding flags to force WebFeed to fetch
// without subscriptions.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeed,
kWebFeedOnboarding},
disabled_features = {};
features.InitWithFeatures(enabled_features, disabled_features);
{
// Load both feeds and allow them to fetch network contents.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices",
for_you_surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 2 slices",
web_feed_surface.DescribeUpdates());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
}
// Both surfaces have been destroyed, so wait for the model to unload.
WaitForModelToAutoUnload();
// Invalidate with an unknown feed and recreate both surfaces. None should
// refresh from network.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->InvalidateContentCacheFor(StreamKind::kUnknown);
{
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices",
for_you_surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 2 slices",
web_feed_surface.DescribeUpdates());
}
EXPECT_FALSE(response_translator_.InjectedResponseConsumed());
}
TEST_F(FeedApiTest, InvalidateFeedCache_DoesNotRefreshOtherFeed) {
// Enable WebFeed and WebFeedOnboarding flags to force WebFeed to fetch
// without subscriptions.
base::test::ScopedFeatureList features;
std::vector<base::test::FeatureRef> enabled_features = {kWebFeed,
kWebFeedOnboarding},
disabled_features = {};
features.InitWithFeatures(enabled_features, disabled_features);
{
// Load both feeds and allow them to fetch network contents.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices",
for_you_surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 2 slices",
web_feed_surface.DescribeUpdates());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
}
// Both surfaces have been destroyed, so wait for the model to unload.
WaitForModelToAutoUnload();
// Invalidate only the WebFeed feed and recreate both surfaces. Only the
// WebFeed should refresh from network.
response_translator_.InjectResponse(MakeTypicalRefreshModelState());
stream_->InvalidateContentCacheFor(StreamKind::kFollowing);
{
TestForYouSurface for_you_surface(stream_.get());
TestWebFeedSurface web_feed_surface(stream_.get());
WaitForIdleTaskQueue();
ASSERT_EQ("loading -> [user@foo] 2 slices",
for_you_surface.DescribeUpdates());
ASSERT_EQ("loading -> [user@foo] 3 slices",
web_feed_surface.DescribeUpdates());
}
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
}
TEST_F(FeedApiTest, FeedCloseRefresh_Scroll) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Simulate content being viewed. This shouldn't schedule a refresh itself,
// but it's required in order for scrolling to schedule a refresh.
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
EXPECT_EQ(base::Seconds(0),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Scrolling should cause a refresh to be scheduled.
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
refresh_scheduler_.Clear();
// Scrolling shouldn't schedule a refresh for the next few minutes.
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
// Scheduler shouldn't have been called yet.
EXPECT_EQ(base::Seconds(0),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
refresh_scheduler_.Clear();
task_environment_.FastForwardBy(base::Minutes(5) + base::Seconds(1));
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_Open) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Opening should cause a refresh to be scheduled.
stream_->ReportOpenAction(GURL("http://example.com"),
StreamType(StreamKind::kForYou), "",
OpenActionType::kDefault);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_OpenInNewTab) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Should cause a refresh to be scheduled.
stream_->ReportOpenAction(GURL("http://example.com"),
StreamType(StreamKind::kForYou), "",
OpenActionType::kNewTab);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_ManualRefreshResetsCoalesceTimestamp) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Simulate content being viewed. This shouldn't schedule a refresh itself,
// but it's required in order for later interaction to schedule a refresh.
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
EXPECT_EQ(base::Seconds(0),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Should cause a refresh to be scheduled.
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
refresh_scheduler_.Clear();
// Manual refresh resets the anti-jank timestamp, so the next
// ReportStreamScrolled() should update the schedule.
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_FeedViewed) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "false"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
// The schedule should have been updated.
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Only a surface's first view should cause the schedule to be set.
refresh_scheduler_.Clear();
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
// Zero means the scheudle wasn't updated.
EXPECT_EQ(base::Seconds(0),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
task_environment_.AdvanceClock(base::Minutes(6));
// Opening another surface should cause a refresh to be scheduled.
refresh_scheduler_.Clear();
TestForYouSurface surface2(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface2.GetSurfaceId());
// The schedule should have been updated.
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
task_environment_.AdvanceClock(base::Minutes(6));
// Leaving the surface and returning should schedule a refresh.
refresh_scheduler_.Clear();
surface.Detach();
WaitForIdleTaskQueue();
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
// The schedule should have been updated.
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_ExistingScheduleGetsReplaced) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
// Inject a typical network response, with a server-defined request schedule.
{
RequestSchedule schedule;
schedule.anchor_time = kTestTimeEpoch;
schedule.refresh_offsets = {base::Minutes(12), base::Minutes(24)};
RefreshResponseData response_data;
response_data.model_update_request = MakeTypicalInitialModelState();
response_data.request_schedule = schedule;
response_translator_.InjectResponse(std::move(response_data));
}
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Verify the first refresh was scheduled (epoch + 12 - 10)
EXPECT_EQ(base::Minutes(2),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Simulate content being viewed. This shouldn't schedule a refresh itself,
// but it's required in order for later interaction to schedule a refresh.
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
// Should cause a refresh to be scheduled.
stream_->ReportStreamScrolled(StreamType(StreamKind::kForYou), 1);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_Retry) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "false"}});
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Update the schedule.
stream_->ReportFeedViewed(StreamType(StreamKind::kForYou),
surface.GetSurfaceId());
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Simulate an allowed refresh that failed.
surface.Detach();
task_environment_.FastForwardBy(base::Minutes(35));
WaitForIdleTaskQueue();
refresh_scheduler_.Clear();
FeedNetwork::RawResponse raw_response;
raw_response.response_info.status_code = 400;
network_.InjectApiRawResponse<QueryBackgroundFeedDiscoverApi>(raw_response);
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// The next one should have been scheduled for 60 minutes after the anchor
// time, and the clock advanced 35 minutes, so the next one should be
// scheduled 25 minutes from now.
EXPECT_EQ(std::set<RefreshTaskId>({RefreshTaskId::kRefreshForYouFeed}),
refresh_scheduler_.completed_tasks);
EXPECT_EQ(base::Minutes(25),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Same thing again. There should be one more scheduled retry.
task_environment_.FastForwardBy(base::Minutes(35));
refresh_scheduler_.Clear();
network_.InjectApiRawResponse<QueryBackgroundFeedDiscoverApi>(raw_response);
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
// The next one should have been scheduled for 90 minutes after the anchor
// time, and the clock advanced 70 minutes total, so the next one should be
// scheduled 20 minutes from now.
EXPECT_EQ(std::set<RefreshTaskId>({RefreshTaskId::kRefreshForYouFeed}),
refresh_scheduler_.completed_tasks);
EXPECT_EQ(base::Minutes(20),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
}
TEST_F(FeedApiTest, FeedCloseRefresh_RequestType) {
// Sometimes the clock starts near zero; move it forward just in case.
task_environment_.AdvanceClock(base::Minutes(10));
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeatureWithParameters(
kFeedCloseRefresh, {{"require_interaction", "true"}});
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Opening should cause a refresh to be scheduled.
stream_->ReportOpenAction(GURL("http://example.com"),
StreamType(StreamKind::kForYou), "",
OpenActionType::kDefault);
EXPECT_EQ(base::Minutes(30),
refresh_scheduler_
.scheduled_run_times[RefreshTaskId::kRefreshForYouFeed]);
// Close the surface and unload the model.
surface.Detach();
task_environment_.FastForwardBy(base::Seconds(2));
WaitForIdleTaskQueue();
// Do the refresh.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
stream_->ExecuteRefreshTask(RefreshTaskId::kRefreshForYouFeed);
WaitForIdleTaskQueue();
ASSERT_TRUE(refresh_scheduler_.completed_tasks.count(
RefreshTaskId::kRefreshForYouFeed));
EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork,
metrics_reporter_->background_refresh_status);
EXPECT_TRUE(network_.query_request_sent);
// Request reason should be set.
EXPECT_EQ(feedwire::FeedQuery::APP_CLOSE_REFRESH,
network_.query_request_sent->feed_request().feed_query().reason());
EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
}
TEST_F(FeedApiTest, CheckDuplicatedContents) {
Config config;
config.max_most_recent_viewed_content_hashes = 6;
SetFeedConfigForTesting(config);
StreamModelUpdateRequestGenerator model_generator;
TestForYouSurface surface;
{
base::HistogramTester histograms;
std::vector<int> num_ids({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
response_translator_.InjectResponse(
model_generator.MakeFirstPageWithSpecificContents(num_ids));
surface.Attach(stream_.get());
WaitForIdleTaskQueue();
histograms.ExpectTotalCount(
"ContentSuggestions.Feed.ContentDuplication2.Position1", 0);
histograms.ExpectTotalCount(
"ContentSuggestions.Feed.ContentDuplication2.Position2", 0);
histograms.ExpectTotalCount(
"ContentSuggestions.Feed.ContentDuplication2.Position3", 0);
histograms.ExpectTotalCount(
"ContentSuggestions.Feed.ContentDuplication2.Top10", 0);
histograms.ExpectTotalCount(
"ContentSuggestions.Feed.ContentDuplication2.ForAll", 0);
}
SurfaceId surface_id = surface.GetSurfaceId();
StreamType stream_type = surface.GetStreamType();
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.initial_state->updated_slices(0).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.initial_state->updated_slices(1).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.initial_state->updated_slices(2).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.initial_state->updated_slices(6).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.initial_state->updated_slices(8).slice().slice_id());
// Viewed contents: 0, 1, 2, 6, 8
{
base::HistogramTester histograms;
std::vector<int> num_ids(
{7, 11, 6, 13, 14, 2, 16, 1, 5, 19, 12, 10, 8, 19});
response_translator_.InjectResponse(
model_generator.MakeFirstPageWithSpecificContents(num_ids));
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position1", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position2", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position3", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.First10", 30, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.All", 28, 1);
}
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(1).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(3).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(4).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(5).slice().slice_id());
// Viewed contents: (0, 1,) 6, 8, 11, 13, 14, 2
{
base::HistogramTester histograms;
std::vector<int> num_ids(
{8, 1, 9, 2, 30, 31, 5, 10, 12, 13, 32, 33, 14, 6});
response_translator_.InjectResponse(
model_generator.MakeFirstPageWithSpecificContents(num_ids));
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position1", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position2", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position3", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.First10", 40, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.All", 42, 1);
}
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(0).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(4).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(5).slice().slice_id());
stream_->ReportSliceViewed(
surface_id, stream_type,
surface.update->updated_slices(6).slice().slice_id());
// Viewed contents: (6, 11, 13), 14, 2, 8, 30, 31, 5
// Simulate a Chrome restart.
CreateStream();
TestForYouSurface surface2(stream_.get());
WaitForIdleTaskQueue();
{
base::HistogramTester histograms;
std::vector<int> num_ids(
{15, 11, 16, 2, 40, 41, 8, 42, 1, 43, 44, 14, 45, 47});
response_translator_.InjectResponse(
model_generator.MakeFirstPageWithSpecificContents(num_ids));
stream_->ManualRefresh(StreamType(StreamKind::kForYou), base::DoNothing());
WaitForIdleTaskQueue();
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position1", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position2", true, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.Position3", false, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.First10", 30, 1);
histograms.ExpectUniqueSample(
"ContentSuggestions.Feed.ContentDuplication2.All", 28, 1);
}
}
TEST_F(FeedApiTest, GetRequestMetadataForSignedOutUser) {
TestForYouSurface surface(stream_.get());
account_info_ = {};
is_sync_on_ = false;
RequestMetadata metadata =
stream_->GetRequestMetadata(StreamType(StreamKind::kForYou), false);
ASSERT_EQ(metadata.sign_in_status,
feedwire::ChromeSignInStatus::NOT_SIGNED_IN);
}
TEST_F(FeedApiTest, GetRequestMetadataForSignedInButNotSyncedUser) {
TestForYouSurface surface(stream_.get());
is_sync_on_ = false;
RequestMetadata metadata =
stream_->GetRequestMetadata(StreamType(StreamKind::kForYou), false);
ASSERT_EQ(metadata.sign_in_status,
feedwire::ChromeSignInStatus::SIGNED_IN_WITHOUT_SYNC);
}
TEST_F(FeedApiTest, GetRequestMetadataForSyncedUser) {
TestForYouSurface surface(stream_.get());
is_sync_on_ = true;
RequestMetadata metadata =
stream_->GetRequestMetadata(StreamType(StreamKind::kForYou), false);
ASSERT_EQ(metadata.sign_in_status, feedwire::ChromeSignInStatus::SYNCED);
}
TEST_F(FeedApiTest, GetRequestMetadataForSigninDisallowedUser) {
TestForYouSurface surface(stream_.get());
account_info_ = {};
is_sync_on_ = false;
is_signin_allowed_ = false;
RequestMetadata metadata =
stream_->GetRequestMetadata(StreamType(StreamKind::kForYou), false);
ASSERT_EQ(metadata.sign_in_status,
feedwire::ChromeSignInStatus::SIGNIN_DISALLOWED_BY_CONFIG);
}
TEST_F(FeedApiTest, GetRequestMetadataForSigninDisallowedUserWhenSignedIn) {
TestForYouSurface surface(stream_.get());
is_sync_on_ = false;
is_signin_allowed_ = false;
RequestMetadata metadata =
stream_->GetRequestMetadata(StreamType(StreamKind::kForYou), false);
ASSERT_EQ(metadata.sign_in_status,
feedwire::ChromeSignInStatus::SIGNED_IN_WITHOUT_SYNC);
}
TEST_F(FeedApiTest, ClearAllOnSigninAllowedPrefChange) {
CallbackReceiver<> on_clear_all;
on_clear_all_ = on_clear_all.BindRepeating();
// Fetch a feed, so that there's stored data.
response_translator_.InjectResponse(MakeTypicalInitialModelState());
TestForYouSurface surface(stream_.get());
WaitForIdleTaskQueue();
// Set signin allowed pref to false. It should perform a ClearAll.
profile_prefs_.SetBoolean(::prefs::kSigninAllowed, false);
WaitForIdleTaskQueue();
EXPECT_TRUE(on_clear_all.called());
// Re-create the feed, and verify ClearAll isn't called again.
on_clear_all.Clear();
WaitForIdleTaskQueue();
EXPECT_FALSE(on_clear_all.called());
}
// Keep instantiations at the bottom.
INSTANTIATE_TEST_SUITE_P(FeedApiTest,
FeedStreamTestForAllStreamTypes,
::testing::Values(StreamType(StreamKind::kForYou),
StreamType(StreamKind::kFollowing)),
::testing::PrintToStringParamName());
INSTANTIATE_TEST_SUITE_P(FeedApiTest,
FeedNetworkEndpointTest,
::testing::Combine(::testing::Bool(),
::testing::Bool()));
} // namespace
} // namespace test
} // namespace feed