blob: b73ff4819319bc14b37afd0d9ae57b45e8742a12 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <type_traits>
#include <unordered_map>
#include "base/feature_list.h"
#include "base/location.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache_test_util.h"
#include "content/browser/renderer_host/pending_beacon_host.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/permission_controller_delegate.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
namespace content {
MATCHER(IsFrameVisible,
base::StrCat({"Frame is", negation ? " not" : "", " visible"})) {
return arg->GetVisibilityState() == PageVisibilityState::kVisible;
}
MATCHER(IsFrameHidden,
base::StrCat({"Frame is", negation ? " not" : "", " hidden"})) {
return arg->GetVisibilityState() == PageVisibilityState::kHidden;
}
class PendingBeaconTimeoutBrowserTestBase : public ContentBrowserTest {
protected:
using FeaturesType = std::vector<base::test::FeatureRefAndParams>;
using DisabledFeaturesType = std::vector<base::test::FeatureRef>;
void SetUp() override {
feature_list_.InitWithFeaturesAndParameters(GetEnabledFeatures(),
GetDisabledFeatures());
ContentBrowserTest::SetUp();
}
virtual const FeaturesType& GetEnabledFeatures() = 0;
virtual const DisabledFeaturesType& GetDisabledFeatures() {
static const DisabledFeaturesType disabled_features = {};
return disabled_features;
}
void SetUpOnMainThread() override {
histogram_tester_ = std::make_unique<base::HistogramTester>();
CheckPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
blink::mojom::PermissionStatus::GRANTED);
// TODO(crbug.com/1293679): Update ContentBrowserTest to support overriding
// permissions.
host_resolver()->AddRule("*", "127.0.0.1");
// Initializes an HTTPS server, as the PendingBeacon API is only supported
// in secure context.
https_test_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_test_server_->ServeFilesFromSourceDirectory(GetTestDataFilePath());
https_test_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
net::test_server::RegisterDefaultHandlers(https_test_server_.get());
// Using `base::Unretained()` as `https_test_server()` is owned by this
// class and and should not be able to outlive.
https_test_server_->RegisterDefaultHandler(base::BindRepeating(
&PendingBeaconTimeoutBrowserTestBase::HandleBeaconRequest,
base::Unretained(this)));
}
void TearDownOnMainThread() override {
histogram_tester_.reset();
ContentBrowserTest::SetUpOnMainThread();
}
net::EmbeddedTestServer* https_test_server() {
return https_test_server_.get();
}
// Runs JS `script` in page A, and then navigates to page B.
void RunScriptInANavigateToB(const std::string& script) {
RunScriptInA(script);
// Navigate to B.
ASSERT_TRUE(
NavigateToURL(https_test_server()->GetURL("b.test", "/title1.html")));
}
// Runs JS `script` in page A.
void RunScriptInA(const std::string& script) {
// Navigate to A.
ASSERT_TRUE(
NavigateToURL(https_test_server()->GetURL("a.test", "/title1.html")));
// Execute `script` in A.
ASSERT_TRUE(ExecJs(web_contents(), script));
}
// Registers a request monitor to wait for `total_beacon` beacons received,
// and then starts the test server.
void RegisterBeaconRequestMonitor(const size_t total_beacon) {
// Using base::Unretained() as `https_test_server()` is owned by
// `content::BrowserTestBase` and should not be able to outlive.
https_test_server()->RegisterRequestMonitor(base::BindRepeating(
&PendingBeaconTimeoutBrowserTestBase::MonitorBeaconRequest,
base::Unretained(this), total_beacon));
ASSERT_TRUE(https_test_server()->Start());
}
// Waits for `kBeaconEndpoint` to be requested `total_beacon` times.
// If `sent_beacon_count_` does not yet reach `total_beacon`, a RunLoop will
// be created and runs until it is stopped by `MonitorBeaconRequest`.
void WaitForAllBeaconsSent(size_t total_beacon) {
{
base::AutoLock auto_lock(count_lock_);
if (sent_beacon_count_ >= total_beacon)
return;
}
{
base::AutoLock auto_lock(count_lock_);
waiting_run_loop_ = std::make_unique<base::RunLoop>(
base::RunLoop::Type::kNestableTasksAllowed);
}
waiting_run_loop_->Run();
{
base::AutoLock auto_lock(count_lock_);
waiting_run_loop_.reset();
}
// Tries to wait such that browser can process the responses from http
// server. (No guarantee)
base::RunLoop().RunUntilIdle();
}
WebContents* web_contents() const { return shell()->web_contents(); }
RenderFrameHostWrapper& current_document() {
current_document_ =
std::make_unique<RenderFrameHostWrapper>(current_frame_host());
return *current_document_;
}
// Caution: the returned might already be killed if BFCache it not working.
RenderFrameHostWrapper& previous_document() {
CHECK(previous_document_);
CHECK(!previous_document_->IsDestroyed());
return *previous_document_;
}
bool WaitUntilPreviousDocumentDeleted() {
CHECK(previous_document_);
return previous_document_->WaitUntilRenderFrameDeleted();
}
bool NavigateToURL(const GURL& url) {
previous_document_ =
std::make_unique<RenderFrameHostWrapper>(current_frame_host());
return content::NavigateToURL(web_contents(), url);
}
size_t sent_beacon_count() {
base::AutoLock auto_lock(count_lock_);
return sent_beacon_count_;
}
void CheckPermissionStatus(blink::PermissionType permission_type,
blink::mojom::PermissionStatus permission_status) {
auto* permission_controller_delegate =
web_contents()->GetBrowserContext()->GetPermissionControllerDelegate();
base::MockOnceCallback<void(blink::mojom::PermissionStatus)> callback;
EXPECT_CALL(callback, Run(permission_status));
permission_controller_delegate->RequestPermission(
permission_type, current_frame_host(), GURL("127.0.0.1"),
/*user_gesture=*/true, callback.Get());
}
const base::HistogramTester& histogram_tester() { return *histogram_tester_; }
void ExpectActions(
const std::unordered_map<PendingBeaconHost::Action, size_t>& actions) {
for (const auto& [action, count] : actions) {
histogram_tester().ExpectBucketCount("PendingBeaconHost.Action", action,
count);
}
}
void ExpectBatchActions(
const std::unordered_map<PendingBeaconHost::BatchAction, size_t>&
batch_actions) {
for (const auto& [batch_action, count] : batch_actions) {
histogram_tester().ExpectBucketCount("PendingBeaconHost.BatchAction",
batch_action, count);
}
}
void ExpectBatchAction(const PendingBeaconHost::BatchAction& batch_action,
size_t count) {
for (auto e = PendingBeaconHost::BatchAction::kNone;
e != PendingBeaconHost::BatchAction::kMaxValue;
e = PendingBeaconHost::BatchAction(static_cast<int>(e) + 1)) {
const size_t expected_count = e == batch_action ? count : 0;
histogram_tester().ExpectBucketCount("PendingBeaconHost.BatchAction", e,
expected_count);
}
}
void ExpectNoBatchAction() {
ExpectBatchActions(
{{PendingBeaconHost::BatchAction::kSendAllOnNavigation, 0},
{PendingBeaconHost::BatchAction::kSendAllOnProcessExit, 0},
{PendingBeaconHost::BatchAction::kSendAllOnHostDestroy, 0}});
}
// Expect the given `count` number of beacons are sent by `batch_action`.
void ExpectSendByBrowserBatchAction(
const PendingBeaconHost::BatchAction& batch_action,
size_t count) {
ExpectActions({{PendingBeaconHost::Action::kCreate, count},
// Expect to not be sent by single beacon's sendNow().
{PendingBeaconHost::Action::kSend, 0},
{PendingBeaconHost::Action::kNetworkSend, count},
// No guarantee RFH can be alive when receiving response.
// {PendingBeaconHost::Action::kNetworkComplete, count},
{PendingBeaconHost::Action::kDelete, 0}});
ExpectBatchAction(batch_action, count);
}
// Expect the given `count` number of beacons are sent by a `sendNow() request
// from renderer. Note that `sendNow()` can be triggered either by JS call or
// by timers for `backgroundTimeout`/`timeout`.
void ExpectSendByRendererAction(size_t count) {
ExpectActions({{PendingBeaconHost::Action::kCreate, count},
// Expect to be sent by beacon's sendNow() from renderer.
{PendingBeaconHost::Action::kSend, count},
{PendingBeaconHost::Action::kNetworkSend, count},
// No guarantee RFH can be alive when receiving response.
// {PendingBeaconHost::Action::kNetworkComplete, count},
{PendingBeaconHost::Action::kDelete, 0}});
ExpectNoBatchAction();
}
static constexpr char kBeaconEndpoint[] = "/pending_beacon/timeout";
private:
// Waits until `total_beacon` beacons received and stops `waiting_run_loop_`.
// Invoked on `https_test_server()`'s IO Thread, so it's required to use
// a lock to protect shared data `sent_beacon_count_` access.
void MonitorBeaconRequest(const size_t total_beacon,
const net::test_server::HttpRequest& request) {
if (request.relative_url == kBeaconEndpoint) {
{
base::AutoLock auto_lock(count_lock_);
sent_beacon_count_++;
if (sent_beacon_count_ < total_beacon) {
return;
}
}
base::AutoLock auto_lock(count_lock_);
if (waiting_run_loop_) {
waiting_run_loop_->Quit();
}
}
}
// Invoked on `https_test_server()`'s IO Thread.
// PendingBeacon doesn't really look into its response, so this method just
// returns OK status.
std::unique_ptr<net::test_server::HttpResponse> HandleBeaconRequest(
const net::test_server::HttpRequest& request) {
if (request.relative_url != kBeaconEndpoint) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
return response;
}
RenderFrameHost* current_frame_host() {
return web_contents()->GetPrimaryMainFrame();
}
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<net::EmbeddedTestServer> https_test_server_ = nullptr;
base::Lock count_lock_;
size_t sent_beacon_count_ GUARDED_BY(count_lock_) = 0;
std::unique_ptr<base::RunLoop> waiting_run_loop_;
std::unique_ptr<RenderFrameHostWrapper> current_document_ = nullptr;
std::unique_ptr<RenderFrameHostWrapper> previous_document_ = nullptr;
std::unique_ptr<base::HistogramTester> histogram_tester_;
};
class PendingBeaconWithBackForwardCacheMetricsBrowserTestBase
: public PendingBeaconTimeoutBrowserTestBase,
public BackForwardCacheMetricsTestMatcher {
protected:
void SetUpOnMainThread() override {
// TestAutoSetUkmRecorder's constructor requires a sequenced context.
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
PendingBeaconTimeoutBrowserTestBase::SetUpOnMainThread();
}
void TearDownOnMainThread() override {
ukm_recorder_.reset();
PendingBeaconTimeoutBrowserTestBase::TearDownOnMainThread();
}
// `BackForwardCacheMetricsTestMatcher` implementation.
const ukm::TestAutoSetUkmRecorder& ukm_recorder() override {
return *ukm_recorder_;
}
// `BackForwardCacheMetricsTestMatcher` implementation.
const base::HistogramTester& histogram_tester() override {
return PendingBeaconTimeoutBrowserTestBase::histogram_tester();
}
private:
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};
struct TestTimeoutType {
std::string test_case_name;
int32_t timeout;
// Returns true if `timeout` is too short for the test browser to complete a
// navigation.
// Empirically picking 1 second here.
bool IsShortTimeout() const { return timeout < 1000 && timeout >= 0; }
};
// Tests to cover PendingBeacon's backgroundTimeout & timeout behaviors when
// BackForwardCache is off.
//
// Disables BackForwardCache such that a page is discarded right away on user
// navigating to another page.
// And on page discard, pending beacons should be sent out no matter what value
// its backgroundTimeout/timeout is.
class PendingBeaconTimeoutNoBackForwardCacheBrowserTest
: public PendingBeaconTimeoutBrowserTestBase,
public testing::WithParamInterface<TestTimeoutType> {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kPendingBeaconAPI, {{"send_on_navigation", "true"}}}};
return enabled_features;
}
const DisabledFeaturesType& GetDisabledFeatures() override {
static const DisabledFeaturesType disabled_features = {
features::kBackForwardCache};
return disabled_features;
}
};
INSTANTIATE_TEST_SUITE_P(
All,
PendingBeaconTimeoutNoBackForwardCacheBrowserTest,
testing::ValuesIn<std::vector<TestTimeoutType>>({
{"LongTimeout", 600000},
{"OneSecondTimeout", 1000},
{"ShortTimeout", 1},
{"ZeroTimeout", 0},
{"DefaultTimeout", -1}, // default.
{"NegativeTimeout", -600000}, // behaves the same as default.
}),
[](const testing::TestParamInfo<TestTimeoutType>& info) {
return info.param.test_case_name;
});
IN_PROC_BROWSER_TEST_P(PendingBeaconTimeoutNoBackForwardCacheBrowserTest,
SendOnPageDiscardNotUsingBackgroundTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with various backgroundTimeout, which should all
// be sent on page A discard.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: $2});
)",
kBeaconEndpoint, GetParam().timeout));
ASSERT_TRUE(WaitUntilPreviousDocumentDeleted());
// The beacon should have been sent out after the page is gone.
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
// As "send_on_navigation" is on, beacons are not send on page discard.
// TODO(crbug.com/1378833): Update to `kSendAllOnHostDestroy` once completed.
ExpectSendByBrowserBatchAction(
PendingBeaconHost::BatchAction::kSendAllOnNavigation, total_beacon);
}
IN_PROC_BROWSER_TEST_P(PendingBeaconTimeoutNoBackForwardCacheBrowserTest,
SendOnPageDiscardNotUsingLongerTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with various timeout, which should all be sent on
// page A discard.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: $2});
)",
kBeaconEndpoint, GetParam().timeout));
ASSERT_TRUE(WaitUntilPreviousDocumentDeleted());
// The beacon should have been sent out after the page is gone.
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
if (GetParam().IsShortTimeout()) {
// If timeout is too short, beacon sending **may** complete before
// navigating away from page A, which means it won't be triggered by the
// "send on page discard" batch action but by the sendNow() from renderer.
ExpectActions({{PendingBeaconHost::Action::kCreate, total_beacon},
// No guarantee to be sent by single beacon's sendNow().
// {PendingBeaconHost::Action::kSend, 0},
{PendingBeaconHost::Action::kNetworkSend, total_beacon},
// No guarantee RFH can be alive when receiving response.
// {PendingBeaconHost::Action::kNetworkComplete, count},
{PendingBeaconHost::Action::kDelete, 0}});
// No guarantee it won't be send on navigation.
ExpectBatchActions(
{{PendingBeaconHost::BatchAction::kSendAllOnProcessExit, 0},
{PendingBeaconHost::BatchAction::kSendAllOnHostDestroy, 0}});
} else {
// As "send_on_navigation" is on, beacons are not send on page discard.
// TODO(crbug.com/1378833): Update to `kSendAllOnHostDestroy` once
// completed.
ExpectSendByBrowserBatchAction(
PendingBeaconHost::BatchAction::kSendAllOnNavigation, total_beacon);
}
}
// Tests to cover PendingBeacon's backgroundTimeout behaviors.
// Setting a long `PendingBeaconMaxBackgroundTimeoutInMs` (1min) > BFCache
// timeout (5s) so that beacon sending cannot be caused by reaching max
// background timeout limit but only by BFCache eviction if backgroundTimeout
// set >= 5s.
class PendingBeaconBackgroundTimeoutBrowserTest
: public PendingBeaconTimeoutBrowserTestBase {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kPendingBeaconAPI,
{{"PendingBeaconMaxBackgroundTimeoutInMs", "60000"},
// Don't force sending out beacons on pagehide.
{"send_on_navigation", "false"}}},
{features::kBackForwardCache, {{}}},
{features::kBackForwardCacheTimeToLiveControl,
{{"time_to_live_seconds", "5"}}},
// Forces BFCache to work in low memory device.
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
IN_PROC_BROWSER_TEST_F(PendingBeaconBackgroundTimeoutBrowserTest,
SendOnHiddenAfterNavigation) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with 0s backgroundTimeout.
// It should be sent out right on entering `hidden` state after navigating
// away from A.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 0});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
// Triggered by backgroundTimeout's 0s timer from renderer.
ExpectSendByRendererAction(total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconBackgroundTimeoutBrowserTest,
SendOnBackgroundTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with backgroundTimeout (1s) < BFCache TTL (5s).
// The beacon should be sent out on entering `hidden` state but before
// page deletion.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 1000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
// Triggered by backgroundTimeout's timer from renderer.
ExpectSendByRendererAction(total_beacon);
}
// When backgroundTimeout is set, its timer resets every time when the page
// becomes visible if it has not yet expired.
IN_PROC_BROWSER_TEST_F(
PendingBeaconBackgroundTimeoutBrowserTest,
NotSendWhenPageIsRestoredBeforeBackgroundTimeoutExpires) {
const size_t total_beacon = 0;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with backgroundTimeout (3s) < BFCache TTL (5s).
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 3000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The page A becomes visible again, so backgroundTimeout timer should stop.
ASSERT_THAT(current_document(), IsFrameVisible());
// Verify that beacon is not sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconBackgroundTimeoutBrowserTest,
SendOnBackForwardCacheEviction) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with backgroundTimeout (8s) > BFCache TTL (5s)
// The beacon should be sent out on page deletion.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 8000});
)",
kBeaconEndpoint));
ASSERT_TRUE(previous_document().WaitUntilRenderFrameDeleted());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconBackgroundTimeoutBrowserTest,
SendMultipleOnBackgroundTimeout) {
const size_t total_beacon = 5;
RegisterBeaconRequestMonitor(total_beacon);
RunScriptInANavigateToB(JsReplace(R"(
let p1 = new PendingGetBeacon($1, {backgroundTimeout: 200});
let p2 = new PendingGetBeacon($1, {backgroundTimeout: 100});
let p3 = new PendingGetBeacon($1, {backgroundTimeout: 500});
let p4 = new PendingGetBeacon($1, {backgroundTimeout: 700});
let p5 = new PendingGetBeacon($1, {backgroundTimeout: 300});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
// Triggered by backgroundTimeout's timer from renderer.
ExpectSendByRendererAction(total_beacon);
}
// Tests to cover PendingBeacon's timeout behaviors.
// Sets `BeaconTimeToLiveInMs` (10s) > BFCache timeout (5s) to also cover beacon
// sending on page eviction.
using PendingBeaconTimeoutBrowserTest =
PendingBeaconBackgroundTimeoutBrowserTest;
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest, SendOnZeroTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with 0s timeout. It should be sent out right away
// (without the page entering 'hidden' state).
RunScriptInA(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: 0});
)",
kBeaconEndpoint));
ASSERT_THAT(current_document(), IsFrameVisible());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// When timeout is set, it's not relevant whether the page is hidden or not.
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest,
SendOnTimeoutWhenPageIsHidden) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with a timeout which should expire when the page A
// is still hidden.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: 1000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
// Verify that beacon is sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// When timeout is set, it's not relevant whether the page is visible or not.
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest,
SendOnTimeoutWhenPageIsVisible) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with a timeout longer enough such that page can
// experience visible -> hidden -> visible.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: 4000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// beacon is not yet sent.
ASSERT_EQ(sent_beacon_count(), 0u);
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The page A becomes visible again, but timeout timer never stops.
ASSERT_THAT(current_document(), IsFrameVisible());
WaitForAllBeaconsSent(total_beacon);
// Verify that beacon is sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest, SendOnShorterTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with long (5s) timeout. And then quickly updates
// to a very short (0.01s) timeout. The beacon should be sent out right away.
RunScriptInA(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: 5000});
p.timeout = 10;
)",
kBeaconEndpoint));
ASSERT_THAT(current_document(), IsFrameVisible());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest, SendOnlyOnce) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon which should be sent out right way.
// But it won't be sent out twice.
RunScriptInA(JsReplace(R"(
let p = new PendingGetBeacon($1, {timeout: 0});
p.timeout = 1;
)",
kBeaconEndpoint));
ASSERT_THAT(current_document(), IsFrameVisible());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest, SendMultipleOnTimeout) {
const size_t total_beacon = 5;
RegisterBeaconRequestMonitor(total_beacon);
RunScriptInA(JsReplace(R"(
let p1 = new PendingGetBeacon($1, {timeout: 200});
let p2 = new PendingGetBeacon($1, {timeout: 100});
let p3 = new PendingGetBeacon($1, {timeout: 500});
let p4 = new PendingGetBeacon($1, {timeout: 700});
let p5 = new PendingGetBeacon($1, {timeout: 300});
)",
kBeaconEndpoint));
ASSERT_THAT(current_document(), IsFrameVisible());
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// Tests to cover PendingBeacon's backgroundTimeout & timeout mutual behaviors.
// Sets a long BFCache timeout (1min) so that beacon won't be sent out due to
// page eviction.
class PendingBeaconMutualTimeoutWithLongBackForwardCacheTTLBrowserTest
: public PendingBeaconWithBackForwardCacheMetricsBrowserTestBase {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kPendingBeaconAPI,
{// Don't force sending out beacons on pagehide.
{"send_on_navigation", "false"}}},
{features::kBackForwardCache, {{}}},
{features::kBackForwardCacheTimeToLiveControl,
{{"time_to_live_seconds", "60"}}},
// Forces BFCache to work in low memory device.
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
IN_PROC_BROWSER_TEST_F(
PendingBeaconMutualTimeoutWithLongBackForwardCacheTTLBrowserTest,
NotSendWhenPageIsRestoredBeforeBeingEvictedFromBackForwardCache) {
const size_t total_beacon = 0;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with default backgroundTimeout & timeout.
// It should not be sent out as long as the page is alive (not evicted from
// BackForwardCache).
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1);
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The same page A is still alive.
ExpectRestored(FROM_HERE);
// Verify that beacon is not sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// When both backgroundTimeout & timeout is set, whichever expires earlier will
// trigger beacon sending (part 1).
IN_PROC_BROWSER_TEST_F(
PendingBeaconMutualTimeoutWithLongBackForwardCacheTTLBrowserTest,
SendOnEarlierTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with long backgroundTimeout (60s) & short
// timeout (1s).
// The shorter one, i.e. timeout, should be reachable such that the beacon
// can be sent before this test case times out.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 60000, timeout: 1000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
// Verify that beacon is sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// When both backgroundTimeout & timeout is set, whichever expires earlier will
// trigger beacon sending (part 2).
IN_PROC_BROWSER_TEST_F(
PendingBeaconMutualTimeoutWithLongBackForwardCacheTTLBrowserTest,
SendOnEarlierBackgroundTimeout) {
const size_t total_beacon = 1;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon with short backgroundTimeout (1s) & long
// timeout (60s).
// The shorter one, i.e. backgroundTimeout, should be reachable such that the
// beacon can be sent before this test case times out.
RunScriptInANavigateToB(JsReplace(R"(
let p = new PendingGetBeacon($1, {backgroundTimeout: 1000, timeout: 60000});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
WaitForAllBeaconsSent(total_beacon);
// Verify that beacon is sent.
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
// Tests to cover PendingBeacon's behaviors when enabled forced sending on
// pagehide event.
//
// Setting a long `PendingBeaconMaxBackgroundTimeoutInMs` (1min), and a long
// BFCache timeout (1min) so that beacon sending cannot be caused by reaching
// max background timeout limit, and cannot be caused by BFCache eviction.
class PendingBeaconSendOnPagehideBrowserTest
: public PendingBeaconWithBackForwardCacheMetricsBrowserTestBase {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kPendingBeaconAPI,
{{"PendingBeaconMaxBackgroundTimeoutInMs", "60000"},
{"send_on_navigation", "true"}}},
{features::kBackForwardCache, {{}}},
{features::kBackForwardCacheTimeToLiveControl,
{{"time_to_live_seconds", "60"}}},
// Forces BFCache to work in low memory device.
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
IN_PROC_BROWSER_TEST_F(PendingBeaconSendOnPagehideBrowserTest,
SendOnPagehideWhenPageIsPersisted) {
const size_t total_beacon = 3;
RegisterBeaconRequestMonitor(total_beacon);
// Creates 3 pending beacons with default backgroundTimeout & timeout.
// They should be sent out on transitioning to pagehide event.
RunScriptInANavigateToB(JsReplace(R"(
document.title = '';
let p1 = new PendingGetBeacon($1);
let p2 = new PendingPostBeacon($1);
let p3 = new PendingGetBeacon($1);
window.addEventListener('pagehide', (e) => {
document.title = e.persisted + '/' + p1.pending + '/' + p2.pending +
'/' + p3.pending;
});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The same page A is still alive.
ExpectRestored(FROM_HERE);
// All beacons should have been sent out before previous pagehide.
std::u16string expected_title = u"true/false/false/false";
TitleWatcher title_watcher(web_contents(), expected_title);
EXPECT_EQ(title_watcher.WaitAndGetTitle(), expected_title);
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconSendOnPagehideBrowserTest,
SendOnPagehideBeforeBackgroundTimeout) {
const size_t total_beacon = 3;
RegisterBeaconRequestMonitor(total_beacon);
// Creates 3 pending beacons with long backgroundTimeout < BFCache TTL (1min).
// They should be sent out on transitioning to pagehide but before the end of
// backgroundTimeout and before BFCache TTL.
RunScriptInANavigateToB(JsReplace(R"(
document.title = '';
let p1 = new PendingGetBeacon($1, {backgroundTimeout: 20000});
let p2 = new PendingPostBeacon($1, {backgroundTimeout: 15000});
let p3 = new PendingGetBeacon($1, {backgroundTimeout: 10000});
window.addEventListener('pagehide', (e) => {
document.title = e.persisted + '/' + p1.pending + '/' + p2.pending +
'/' + p3.pending;
});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The same page A is still alive.
ExpectRestored(FROM_HERE);
// All beacons should have been sent out.
std::u16string expected_title = u"true/false/false/false";
TitleWatcher title_watcher(web_contents(), expected_title);
EXPECT_EQ(title_watcher.WaitAndGetTitle(), expected_title);
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
IN_PROC_BROWSER_TEST_F(PendingBeaconSendOnPagehideBrowserTest,
SendOnPagehideBeforeTimeout) {
const size_t total_beacon = 3;
RegisterBeaconRequestMonitor(total_beacon);
// Creates 3 pending beacons with long timeout < BFCache TTL (1min).
// They should be sent out on transitioning to pagehide but before the end of
// timeout and before BFCache TTL.
RunScriptInANavigateToB(JsReplace(R"(
document.title = '';
let p1 = new PendingGetBeacon($1, {timeout: 20000});
let p2 = new PendingPostBeacon($1, {timeout: 10000});
let p3 = new PendingGetBeacon($1, {timeout: 15000});
window.addEventListener('pagehide', (e) => {
document.title = e.persisted + '/' + p1.pending + '/' + p2.pending +
'/' + p3.pending;
});
)",
kBeaconEndpoint));
ASSERT_THAT(previous_document(), IsFrameHidden());
// Navigate back to A.
ASSERT_TRUE(HistoryGoBack(web_contents()));
// The same page A is still alive.
ExpectRestored(FROM_HERE);
// All beacons should have been sent out.
std::u16string expected_title = u"true/false/false/false";
TitleWatcher title_watcher(web_contents(), expected_title);
EXPECT_EQ(title_watcher.WaitAndGetTitle(), expected_title);
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
}
class PendingBeaconRendererProcessExitBrowserTest
: public PendingBeaconTimeoutBrowserTestBase {
protected:
const FeaturesType& GetEnabledFeatures() override {
static const FeaturesType enabled_features = {
{blink::features::kPendingBeaconAPI, {}},
// Forces BFCache to work in low memory device so that a page won't be
// killed by normal page discard.
{features::kBackForwardCacheMemoryControls,
{{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}};
return enabled_features;
}
};
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
// Disabled due to failures on various Mac/Linux builders.
// TODO(crbug.com/1382713) Reenable the test.
#define MAYBE_SendAllOnProcessCrash DISABLED_SendAllOnProcessCrash
#else
#define MAYBE_SendAllOnProcessCrash SendAllOnProcessCrash
#endif
IN_PROC_BROWSER_TEST_F(PendingBeaconRendererProcessExitBrowserTest,
MAYBE_SendAllOnProcessCrash) {
const size_t total_beacon = 2;
RegisterBeaconRequestMonitor(total_beacon);
// Creates a pending beacon that will only be sent on page discard or crash.
// Make beacon creations within a Promise to ensure they can all be created
// before executing next statement.
RunScriptInA(JsReplace(R"(
new Promise(resolve => {
new PendingGetBeacon($1);
new PendingPostBeacon($1);
resolve();
});
)",
kBeaconEndpoint));
ASSERT_EQ(sent_beacon_count(), 0u);
ASSERT_TRUE(
PendingBeaconHost::GetForCurrentDocument(current_document().get()));
// Make the renderer crash.
CrashTab(web_contents());
// The RenderFrame is dead but the attached beacon host is still alive.
ASSERT_FALSE(current_document()->IsRenderFrameLive());
ASSERT_TRUE(
PendingBeaconHost::GetForCurrentDocument(current_document().get()));
WaitForAllBeaconsSent(total_beacon);
EXPECT_EQ(sent_beacon_count(), total_beacon);
ExpectSendByBrowserBatchAction(
PendingBeaconHost::BatchAction::kSendAllOnProcessExit, total_beacon);
}
} // namespace content