From 2be095e89fa5007ea5bc1bfe4419165471be3454 Mon Sep 17 00:00:00 2001 From: Ming-Ying Chung Date: Thu, 15 Sep 2022 06:44:24 +0000 Subject: [PATCH] [beacon-api] Sends all beacons (of a document/RFH) on user navigates away to a different document. This is to mitigate potential privacy issue that when network changes after users think they have left a page, beacons queued in that page still exist and get sent through the new network, which leaks navigation history to the new network. See [1] for more details. This CL adds the following changes: 1) In browser: Add a call to `PendingBeaconHost::SendAllOnPagehide()` from `RenderFrameHostManager::UnloadOldFrame()` to let old RFH send out all its pending beacons before it is put into BFCache or being unloaded. 2) In renderer: Update `PendingBeaconDispatcher` to be called on to update all its pending beacons' states before a `pagehide` event is dispatched to event listeners. This change makes `backgroundTimeout` property useless in the scenario that users navigate away to a different page. But it can still work in other cases like when tab is minimized or when switching tabs. [1]: https://github.com/WICG/unload-beacon/issues/30 Bug: 1293679 Change-Id: I5a7ddf4284717e1af482286dd6f56344d4ef4b26 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3881967 Reviewed-by: Rakina Zata Amni Reviewed-by: Daniel Cheng Commit-Queue: Ming-Ying Chung Cr-Commit-Position: refs/heads/main@{#1047323} --- .../pending_beacon_browsertest.cc | 256 +++++++++++++----- .../renderer_host/pending_beacon_host.cc | 24 ++ .../renderer_host/pending_beacon_host.h | 58 ++-- .../pending_beacon_host_unittest.cc | 176 ++++++------ .../renderer_host/render_frame_host_impl.cc | 11 + .../renderer_host/render_frame_host_impl.h | 6 + .../render_frame_host_manager.cc | 7 +- third_party/blink/common/features.cc | 2 + third_party/blink/public/common/features.h | 5 + .../public/mojom/frame/pending_beacon.mojom | 13 +- .../blink/renderer/core/frame/build.gni | 1 + .../renderer/core/frame/local_dom_window.cc | 9 + .../renderer/core/frame/pending_beacon.cc | 8 +- .../renderer/core/frame/pending_beacon.h | 6 + .../core/frame/pending_beacon_dispatcher.cc | 39 +++ .../core/frame/pending_beacon_dispatcher.h | 57 +++- .../frame/pending_beacon_dispatcher_test.cc | 170 ++++++++---- .../core/frame/pending_beacon_test.cc | 238 ++++++++++++++++ 18 files changed, 868 insertions(+), 218 deletions(-) create mode 100644 third_party/blink/renderer/core/frame/pending_beacon_test.cc diff --git a/content/browser/renderer_host/pending_beacon_browsertest.cc b/content/browser/renderer_host/pending_beacon_browsertest.cc index eb437e8fa6804a..6ffe0d23d49700 100644 --- a/content/browser/renderer_host/pending_beacon_browsertest.cc +++ b/content/browser/renderer_host/pending_beacon_browsertest.cc @@ -43,6 +43,15 @@ MATCHER(IsFrameHidden, class PendingBeaconTimeoutBrowserTestBase : public ContentBrowserTest { protected: + using FeaturesType = + std::vector; + + void SetUp() override { + feature_list_.InitWithFeaturesAndParameters(GetEnabledFeatures(), {}); + ContentBrowserTest::SetUp(); + } + virtual const FeaturesType& GetEnabledFeatures() = 0; + void SetUpOnMainThread() override { CheckPermissionStatus(blink::PermissionType::BACKGROUND_SYNC, blink::mojom::PermissionStatus::GRANTED); @@ -190,6 +199,8 @@ class PendingBeaconTimeoutBrowserTestBase : public ContentBrowserTest { return web_contents()->GetPrimaryMainFrame(); } + base::test::ScopedFeatureList feature_list_; + base::Lock count_lock_; size_t sent_beacon_count_ GUARDED_BY(count_lock_) = 0; std::unique_ptr waiting_run_loop_; @@ -198,6 +209,37 @@ class PendingBeaconTimeoutBrowserTestBase : public ContentBrowserTest { std::unique_ptr previous_document_ = nullptr; }; +class PendingBeaconWithBackForwardCacheMetricsBrowserTestBase + : public PendingBeaconTimeoutBrowserTestBase, + public BackForwardCacheMetricsTestMatcher { + protected: + void SetUpOnMainThread() override { + // TestAutoSetUkmRecorder's constructor requires a sequenced context. + ukm_recorder_ = std::make_unique(); + histogram_tester_ = std::make_unique(); + PendingBeaconTimeoutBrowserTestBase::SetUpOnMainThread(); + } + + void TearDownOnMainThread() override { + ukm_recorder_.reset(); + histogram_tester_.reset(); + PendingBeaconTimeoutBrowserTestBase::TearDownOnMainThread(); + } + + // `BackForwardCacheMetricsTestMatcher` implementation. + const ukm::TestAutoSetUkmRecorder& ukm_recorder() override { + return *ukm_recorder_; + } + // `BackForwardCacheMetricsTestMatcher` implementation. + const base::HistogramTester& histogram_tester() override { + return *histogram_tester_; + } + + private: + std::unique_ptr ukm_recorder_; + std::unique_ptr histogram_tester_; +}; + struct TestTimeoutType { std::string test_case_name; int32_t timeout; @@ -213,16 +255,12 @@ class PendingBeaconTimeoutNoBackForwardCacheBrowserTest : public PendingBeaconTimeoutBrowserTestBase, public testing::WithParamInterface { protected: - void SetUpCommandLine(base::CommandLine* command_line) override { - feature_list_.InitWithFeaturesAndParameters( - {{blink::features::kPendingBeaconAPI, {}}, - {features::kBackForwardCache, {{"cache_size", "0"}}}}, - {}); - PendingBeaconTimeoutBrowserTestBase::SetUpCommandLine(command_line); + const FeaturesType& GetEnabledFeatures() override { + static const FeaturesType enabled_features = { + {blink::features::kPendingBeaconAPI, {{"send_on_navigation", "true"}}}, + {features::kBackForwardCache, {{"cache_size", "0"}}}}; + return enabled_features; } - - private: - base::test::ScopedFeatureList feature_list_; }; INSTANTIATE_TEST_SUITE_P( @@ -253,7 +291,7 @@ IN_PROC_BROWSER_TEST_P(PendingBeaconTimeoutNoBackForwardCacheBrowserTest, kBeaconEndpoint, GetParam().timeout)); ASSERT_TRUE(WaitUntilPreviousDocumentDeleted()); - WaitForAllBeaconsSent(total_beacon); + // The beacon should have been sent out after the page is gone. EXPECT_EQ(sent_beacon_count(), total_beacon); } @@ -270,7 +308,7 @@ IN_PROC_BROWSER_TEST_P(PendingBeaconTimeoutNoBackForwardCacheBrowserTest, kBeaconEndpoint, GetParam().timeout)); ASSERT_TRUE(WaitUntilPreviousDocumentDeleted()); - WaitForAllBeaconsSent(total_beacon); + // The beacon should have been sent out after the page is gone. EXPECT_EQ(sent_beacon_count(), total_beacon); } @@ -282,21 +320,19 @@ IN_PROC_BROWSER_TEST_P(PendingBeaconTimeoutNoBackForwardCacheBrowserTest, class PendingBeaconBackgroundTimeoutBrowserTest : public PendingBeaconTimeoutBrowserTestBase { protected: - void SetUpCommandLine(base::CommandLine* command_line) override { - feature_list_.InitWithFeaturesAndParameters( - {{blink::features::kPendingBeaconAPI, - {{"PendingBeaconMaxBackgroundTimeoutInMs", "10000"}}}, - {features::kBackForwardCache, - {{"TimeToLiveInBackForwardCacheInSeconds", "5"}}}, - // Forces BFCache to work in low memory device. - {features::kBackForwardCacheMemoryControls, - {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}, - {}); - PendingBeaconTimeoutBrowserTestBase::SetUpCommandLine(command_line); + 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, + {{"TimeToLiveInBackForwardCacheInSeconds", "5"}}}, + // Forces BFCache to work in low memory device. + {features::kBackForwardCacheMemoryControls, + {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}; + return enabled_features; } - - private: - base::test::ScopedFeatureList feature_list_; }; IN_PROC_BROWSER_TEST_F(PendingBeaconBackgroundTimeoutBrowserTest, @@ -518,48 +554,20 @@ IN_PROC_BROWSER_TEST_F(PendingBeaconTimeoutBrowserTest, SendMultipleOnTimeout) { // Sets a long BFCache timeout (1min) so that beacon won't be sent out due to // page eviction. class PendingBeaconMutualTimeoutWithLongBackForwardCacheTTLBrowserTest - : public PendingBeaconTimeoutBrowserTestBase, - public BackForwardCacheMetricsTestMatcher { + : public PendingBeaconWithBackForwardCacheMetricsBrowserTestBase { protected: - void SetUpCommandLine(base::CommandLine* command_line) override { - feature_list_.InitWithFeaturesAndParameters( - {{blink::features::kPendingBeaconAPI, {}}, - {features::kBackForwardCache, - {{"TimeToLiveInBackForwardCacheInSeconds", "60"}}}, - // Forces BFCache to work in low memory device. - {features::kBackForwardCacheMemoryControls, - {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}, - {}); - PendingBeaconTimeoutBrowserTestBase::SetUpCommandLine(command_line); - } - - void SetUpOnMainThread() override { - // TestAutoSetUkmRecorder's constructor requires a sequenced context. - ukm_recorder_ = std::make_unique(); - histogram_tester_ = std::make_unique(); - PendingBeaconTimeoutBrowserTestBase::SetUpOnMainThread(); - } - - void TearDownOnMainThread() override { - ukm_recorder_.reset(); - histogram_tester_.reset(); - PendingBeaconTimeoutBrowserTestBase::TearDownOnMainThread(); - } - - // `BackForwardCacheMetricsTestMatcher` implementation. - const ukm::TestAutoSetUkmRecorder& ukm_recorder() override { - return *ukm_recorder_; - } - // `BackForwardCacheMetricsTestMatcher` implementation. - const base::HistogramTester& histogram_tester() override { - return *histogram_tester_; + 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, + {{"TimeToLiveInBackForwardCacheInSeconds", "60"}}}, + // Forces BFCache to work in low memory device. + {features::kBackForwardCacheMemoryControls, + {{"memory_threshold_for_back_forward_cache_in_mb", "0"}}}}; + return enabled_features; } - - private: - base::test::ScopedFeatureList feature_list_; - - std::unique_ptr ukm_recorder_; - std::unique_ptr histogram_tester_; }; IN_PROC_BROWSER_TEST_F( @@ -631,4 +639,122 @@ IN_PROC_BROWSER_TEST_F( 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, + {{"TimeToLiveInBackForwardCacheInSeconds", "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); + 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); + 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); + EXPECT_EQ(sent_beacon_count(), total_beacon); +} + } // namespace content diff --git a/content/browser/renderer_host/pending_beacon_host.cc b/content/browser/renderer_host/pending_beacon_host.cc index bafb968957e8fc..21c1c014e227a2 100644 --- a/content/browser/renderer_host/pending_beacon_host.cc +++ b/content/browser/renderer_host/pending_beacon_host.cc @@ -16,6 +16,7 @@ #include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/mojom/fetch_api.mojom.h" +#include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/permissions/permission_utils.h" namespace content { @@ -104,6 +105,7 @@ void PendingBeaconHost::Send( if (beacons.empty()) { return; } + service_->SendBeacons(beacons, shared_url_factory_.get()); } @@ -112,6 +114,28 @@ void PendingBeaconHost::SetReceiver( receiver_.Bind(std::move(receiver)); } +void PendingBeaconHost::SendAllOnNavigation() { + if (!blink::features::kPendingBeaconAPIForcesSendingOnNavigation.Get()) { + return; + } + + // Sends out all `beacons_` ASAP to avoid network change happens. + // This is to mitigate potential privacy issue that when network changes + // after users think they have left a page, beacons queued in that page + // still exist and get sent through the new network, which leaks navigation + // history to the new network. + // See https://github.com/WICG/unload-beacon/issues/30. + + // Swaps out from private field first to make any potential subsequent send + // requests from renderer no-ops. + std::vector> to_send; + to_send.swap(beacons_); + Send(to_send); + + // Now all beacons are gone. + // The renderer-side beacons should update their pending states by themselves. +} + DOCUMENT_USER_DATA_KEY_IMPL(PendingBeaconHost); void Beacon::Deactivate() { diff --git a/content/browser/renderer_host/pending_beacon_host.h b/content/browser/renderer_host/pending_beacon_host.h index adc026f16a48eb..71a312ad8757f1 100644 --- a/content/browser/renderer_host/pending_beacon_host.h +++ b/content/browser/renderer_host/pending_beacon_host.h @@ -5,6 +5,8 @@ #ifndef CONTENT_BROWSER_RENDERER_HOST_PENDING_BEACON_HOST_H_ #define CONTENT_BROWSER_RENDERER_HOST_PENDING_BEACON_HOST_H_ +#include + #include "base/memory/raw_ptr.h" #include "content/common/content_export.h" #include "content/public/browser/document_user_data.h" @@ -36,22 +38,29 @@ class PendingBeaconService; // PendingBeaconHost creates a new Beacon when `CreateBeacon()` is called // remotely from a document in renderer. // -// PendingBeaconHost receives `SendBeacon()` requests initiated from renderer -// and forwards it to PendingBeaconService. The requests can be initiated in one -// of the following scenarios: -// - When JavaScript executes `PendingBeacon.sendNow()`, which connects to -// receiver `Beacon::SendNow()`. -// - When the associated document enters `hidden` state, and the renderer's -// `PendingBeaconDispatcher` schedules and dispatches the request according -// to individual PendingBeacon's backgroundTimeout property. -// - When the individual PendingBeacon's timer of timeout property expires. +// PendingBeaconHost is responsible for preparing beacons and forwards them to +// `PendingBeaconService` for sending. A "beacon-sending" operation can be +// initiated from either the renderer process, or from the browser process: +// +// 1. From renderer. The `SendBeacon()` method handles beacon-sending requests +// initiated from renderer. It can be called in one of the following +// scenarios: +// A. When JavaScript executes `PendingBeacon.sendNow()`, which connects to +// receiver `Beacon::SendNow()`. +// B. When the associated document enters `hidden` state, and the renderer's +// `PendingBeaconDispatcher` schedules and dispatches the request +// according to individual PendingBeacon's backgroundTimeout property. +// C. When the individual PendingBeacon's timer of timeout property expires. // -// PendingBeaconHost is also responsible for triggering the sending of beacons: -// - When the associated document is discarded or deleted, PendingBeaconHost -// sends out all queued beacons from its destructor. -// - TODO(crbug.com/1293679): When the associated document's renderer process -// crashes, PendingBeaconHost sends out all queued beacon after being -// notified by RenderProcessHostDestroyed. +// 2. From browser. PendingBeaconHost can trigger the sending of beacons: +// A. When the associated document is discarded or deleted, PendingBeaconHost +// sends out all queued beacons from its destructor. +// B. TODO(crbug.com/1293679): When the associated document's renderer +// process crashes, PendingBeaconHost sends out all queued beacon after +// being notified by RenderProcessHostDestroyed. +// C. When the associated document enters `pagehide` state, i.e. the user has +// navigated away from the document, PendingBeaconHost sends out all +// queued beacons. class CONTENT_EXPORT PendingBeaconHost : public blink::mojom::PendingBeaconHost, public DocumentUserData { @@ -68,11 +77,24 @@ class CONTENT_EXPORT PendingBeaconHost // Deletes the `beacon` if exists. void DeleteBeacon(Beacon* beacon); // Sends out the `beacon` if exists. + // + // This method handles beacon-sending requests from the renderer. See class + // doc for more details. void SendBeacon(Beacon* beacon); void SetReceiver( mojo::PendingReceiver receiver); + // Forces sending out all `beacons_` on navigating away (pagehide). + // + // Whether or not the page is put into BackForwardCache is not relevant. + // + // "Unlike `SendBeacon()` which is triggered by the renderer, this method is + // called only by the browser process itself. + // + // https://github.com/WICG/unload-beacon/issues/30 + void SendAllOnNavigation(); + private: friend class DocumentUserData; @@ -118,10 +140,11 @@ class Beacon : public blink::mojom::PendingBeacon { mojo::PendingReceiver receiver); ~Beacon() override; + // `blink::mojom::PendingBeacon` overrides (used by the renderer): // Deletes this beacon from its containing PendingBeaconHost. void Deactivate() override; - // Sets request data for the pending beacon. + // // It is only allowed when this beacon's `BeaconMethod` is kPost. // `request_body` must // - Contain only single data element. Complex body is not allowed. @@ -130,12 +153,11 @@ class Beacon : public blink::mojom::PendingBeacon { // requests. void SetRequestData(scoped_refptr request_body, const std::string& content_type) override; - // Sets request url for the pending beacon. + // // The spec only allows GET beacons to update its own URL. So `BeaconMethod` // must be kGet when calling this. void SetRequestURL(const GURL& url) override; - // Sends the beacon immediately, and deletes it from its containing // PendingBeaconHost. void SendNow() override; diff --git a/content/browser/renderer_host/pending_beacon_host_unittest.cc b/content/browser/renderer_host/pending_beacon_host_unittest.cc index 055069c7a5aaa1..53eda24da8017e 100644 --- a/content/browser/renderer_host/pending_beacon_host_unittest.cc +++ b/content/browser/renderer_host/pending_beacon_host_unittest.cc @@ -10,6 +10,7 @@ #include "base/memory/scoped_refptr.h" #include "base/strings/stringprintf.h" #include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" #include "content/browser/renderer_host/pending_beacon_service.h" #include "content/public/browser/permission_result.h" #include "content/public/test/mock_permission_manager.h" @@ -20,8 +21,10 @@ #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" #include "services/network/public/mojom/fetch_api.mojom.h" #include "services/network/test/test_url_loader_factory.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/abseil-cpp/absl/types/optional.h" +#include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/permissions/permission_utils.h" #include "third_party/blink/public/mojom/frame/pending_beacon.mojom-shared.h" #include "third_party/blink/public/mojom/frame/pending_beacon.mojom.h" @@ -29,9 +32,17 @@ namespace content { +struct MockClientBeacon { + MockClientBeacon(const MockClientBeacon&) = delete; + MockClientBeacon& operator=(const MockClientBeacon&) = delete; + MockClientBeacon() = default; + + mojo::Remote remote; +}; + class PendingBeaconHostTestBase : public RenderViewHostTestHarness, - public testing::WithParamInterface { + public ::testing::WithParamInterface { public: PendingBeaconHostTestBase(const PendingBeaconHostTestBase&) = delete; PendingBeaconHostTestBase& operator=(const PendingBeaconHostTestBase&) = @@ -56,6 +67,30 @@ class PendingBeaconHostTestBase PendingBeaconService::GetInstance()); return PendingBeaconHost::GetForCurrentDocument(main_rfh()); } + PendingBeaconHost* host() { + return PendingBeaconHost::GetForCurrentDocument(main_rfh()); + } + + // Ask PendingBeaconHost to create `total` browser-side beacons. + // Returns the mock client beacons that connect to browser-side beacons + // The URLs for the beacons are generated by `CreateBeaconTargetURL()`. + std::vector CreateBeacons(size_t total, + const std::string& method) { + std::vector client_beacons(total); + auto* host = CreateHost(); + for (size_t i = 0; i < total; i++) { + host->CreateBeacon(client_beacons[i].remote.BindNewPipeAndPassReceiver(), + CreateBeaconTargetURL(i), ToBeaconMethod(method)); + } + return client_beacons; + } + std::unique_ptr CreateBeacon(const std::string& method) { + auto client_beacon = std::make_unique(); + auto* host = CreateHost(); + host->CreateBeacon(client_beacon->remote.BindNewPipeAndPassReceiver(), + GURL(kBeaconTargetURL), ToBeaconMethod(method)); + return client_beacon; + } static blink::mojom::BeaconMethod ToBeaconMethod(const std::string& method) { if (method == net::HttpRequestHeaders::kGetMethod) { @@ -104,6 +139,14 @@ class PendingBeaconHostTestBase class PendingBeaconHostTest : public PendingBeaconHostTestBase { protected: + void SetUp() override { + const std::vector + enabled_features = {{blink::features::kPendingBeaconAPI, + {{"send_on_pagehide", "true"}}}}; + feature_list_.InitWithFeaturesAndParameters(enabled_features, {}); + PendingBeaconHostTestBase::SetUp(); + } + // Registers a callback to verify if the most-recent network request's content // matches the given `method` and `url`. void SetExpectNetworkRequest(const base::Location& location, @@ -124,6 +167,9 @@ class PendingBeaconHostTest : public PendingBeaconHostTestBase { } })); } + + private: + base::test::ScopedFeatureList feature_list_; }; INSTANTIATE_TEST_SUITE_P( @@ -138,11 +184,9 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(PendingBeaconHostTest, SendBeacon) { const std::string method = GetParam(); - const auto url = GURL("/test_send_beacon"); - auto* host = CreateHost(); - mojo::Remote remote; - auto receiver = remote.BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), url, ToBeaconMethod(method)); + const auto url = GURL(kBeaconTargetURL); + auto beacon = CreateBeacon(method); + auto& remote = beacon->remote; SetExpectNetworkRequest(FROM_HERE, method, url); remote->SendNow(); @@ -151,51 +195,36 @@ TEST_P(PendingBeaconHostTest, SendBeacon) { TEST_P(PendingBeaconHostTest, SendOneOfBeacons) { const std::string method = GetParam(); - const auto* url = "/test_send_beacon"; const size_t total = 5; // Sends out only the 3rd of 5 created beacons. - auto* host = CreateHost(); - std::vector> remotes(total); - for (size_t i = 0; i < remotes.size(); i++) { - auto receiver = remotes[i].BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), GURL(url + i), - ToBeaconMethod(method)); - } + auto beacons = CreateBeacons(total, method); const size_t sent_beacon_i = 2; - SetExpectNetworkRequest(FROM_HERE, method, GURL(url + sent_beacon_i)); - remotes[sent_beacon_i]->SendNow(); + SetExpectNetworkRequest(FROM_HERE, method, + CreateBeaconTargetURL(sent_beacon_i)); + beacons[sent_beacon_i].remote->SendNow(); ExpectTotalNetworkRequests(FROM_HERE, 1); } TEST_P(PendingBeaconHostTest, SendBeacons) { const std::string method = GetParam(); - const auto* url = "/test_send_beacon"; const size_t total = 5; // Sends out all 5 created beacons, in reversed order. - auto* host = CreateHost(); - std::vector> remotes(total); - for (size_t i = 0; i < remotes.size(); i++) { - auto receiver = remotes[i].BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), GURL(url + i), - ToBeaconMethod(method)); - } - for (int i = remotes.size() - 1; i >= 0; i--) { - SetExpectNetworkRequest(FROM_HERE, method, GURL(url + i)); - remotes[i]->SendNow(); + auto beacons = CreateBeacons(total, method); + for (int i = beacons.size() - 1; i >= 0; i--) { + SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i)); + beacons[i].remote->SendNow(); } ExpectTotalNetworkRequests(FROM_HERE, total); } TEST_P(PendingBeaconHostTest, DeleteAndSendBeacon) { const std::string method = GetParam(); - const auto url = GURL("/test_send_beacon"); - auto* host = CreateHost(); - mojo::Remote remote; - auto receiver = remote.BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), url, ToBeaconMethod(method)); + const auto url = GURL(kBeaconTargetURL); + auto beacon = CreateBeacon(method); + auto& remote = beacon->remote; // Deleted beacon won't be sent out by host. remote->Deactivate(); @@ -205,26 +234,19 @@ TEST_P(PendingBeaconHostTest, DeleteAndSendBeacon) { TEST_P(PendingBeaconHostTest, DeleteOneAndSendOtherBeacons) { const std::string method = GetParam(); - const auto* url = "/test_send_beacon"; const size_t total = 5; // Creates 5 beacons. Deletes the 3rd of them, and sends out the others. - auto* host = CreateHost(); - std::vector> remotes(total); - for (size_t i = 0; i < remotes.size(); i++) { - auto receiver = remotes[i].BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), GURL(url + i), - ToBeaconMethod(method)); - } + auto beacons = CreateBeacons(total, method); const size_t deleted_beacon_i = 2; - remotes[deleted_beacon_i]->Deactivate(); + beacons[deleted_beacon_i].remote->Deactivate(); - for (int i = remotes.size() - 1; i >= 0; i--) { + for (int i = beacons.size() - 1; i >= 0; i--) { if (i != deleted_beacon_i) { - SetExpectNetworkRequest(FROM_HERE, method, GURL(url + i)); + SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i)); } - remotes[i]->SendNow(); + beacons[i].remote->SendNow(); } ExpectTotalNetworkRequests(FROM_HERE, total - 1); } @@ -234,13 +256,7 @@ TEST_P(PendingBeaconHostTest, SendOnDocumentUnloadWithBackgroundSync) { const size_t total = 5; // Creates 5 beacons on the page. - auto* host = CreateHost(); - std::vector> remotes(total); - for (size_t i = 0; i < remotes.size(); i++) { - auto receiver = remotes[i].BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), CreateBeaconTargetURL(i), - ToBeaconMethod(method)); - } + auto beacons = CreateBeacons(total, method); SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC, blink::mojom::PermissionStatus::GRANTED); @@ -256,13 +272,7 @@ TEST_P(PendingBeaconHostTest, const size_t total = 5; // Creates 5 beacons on the page. - auto* host = CreateHost(); - std::vector> remotes(total); - for (size_t i = 0; i < remotes.size(); i++) { - auto receiver = remotes[i].BindNewPipeAndPassReceiver(); - host->CreateBeacon(std::move(receiver), CreateBeaconTargetURL(i), - ToBeaconMethod(method)); - } + auto beacons = CreateBeacons(total, method); SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC, blink::mojom::PermissionStatus::ASK); @@ -272,6 +282,19 @@ TEST_P(PendingBeaconHostTest, ExpectTotalNetworkRequests(FROM_HERE, 0); } +TEST_P(PendingBeaconHostTest, SendOnNavigation) { + const std::string method = GetParam(); + const size_t total = 5; + + // Creates 5 beacons on the page. + auto beacons = CreateBeacons(total, method); + + // Simulates sends on pagehide. + host()->SendAllOnNavigation(); + + ExpectTotalNetworkRequests(FROM_HERE, total); +} + class BeaconTestBase : public PendingBeaconHostTestBase { protected: void TearDown() override { @@ -279,16 +302,6 @@ class BeaconTestBase : public PendingBeaconHostTestBase { PendingBeaconHostTestBase::TearDown(); } - mojo::Remote CreateBeaconAndPassRemote( - const std::string& method) { - const auto url = GURL("/test_send_beacon"); - host_ = CreateHost(); - mojo::Remote remote; - auto receiver = remote.BindNewPipeAndPassReceiver(); - host_->CreateBeacon(std::move(receiver), url, ToBeaconMethod(method)); - return remote; - } - scoped_refptr CreateRequestBody( const std::string& data) { return network::ResourceRequestBody::CreateFromBytes(data.data(), @@ -322,15 +335,22 @@ class BeaconTestBase : public PendingBeaconHostTestBase { return body; } + mojo::Remote& CreateBeaconAndPassRemote( + const std::string& method) { + beacon_ = CreateBeacon(method); + return beacon_->remote; + } + private: // Owned by `main_rfh()`. PendingBeaconHost* host_; + std::unique_ptr beacon_; }; using GetBeaconTest = BeaconTestBase; TEST_F(GetBeaconTest, AttemptToSetRequestDataForGetBeaconAndTerminated) { - auto beacon_remote = + auto& beacon_remote = CreateBeaconAndPassRemote(net::HttpRequestHeaders::kGetMethod); // Intercepts Mojo bad-message error. std::string bad_message; @@ -349,7 +369,7 @@ TEST_F(GetBeaconTest, AttemptToSetRequestDataForGetBeaconAndTerminated) { using PostBeaconTest = BeaconTestBase; TEST_F(PostBeaconTest, AttemptToSetRequestDataWithComplexBodyAndTerminated) { - auto beacon_remote = + auto& beacon_remote = CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod); // Intercepts Mojo bad-message error. std::string bad_message; @@ -366,7 +386,7 @@ TEST_F(PostBeaconTest, AttemptToSetRequestDataWithComplexBodyAndTerminated) { } TEST_F(PostBeaconTest, AttemptToSetRequestDataWithStreamingBodyAndTerminated) { - auto beacon_remote = + auto& beacon_remote = CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod); // Intercepts Mojo bad-message error. std::string bad_message; @@ -383,7 +403,7 @@ TEST_F(PostBeaconTest, AttemptToSetRequestDataWithStreamingBodyAndTerminated) { } TEST_F(PostBeaconTest, AttemptToSetRequestURLForPostBeaconAndTerminated) { - auto beacon_remote = + auto& beacon_remote = CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod); // Intercepts Mojo bad-message error. std::string bad_message; @@ -451,14 +471,14 @@ class PostBeaconRequestDataTest : public BeaconTestBase { })); } - mojo::Remote CreateBeaconAndPassRemote() { + mojo::Remote& CreateBeaconAndPassRemote() { return BeaconTestBase::CreateBeaconAndPassRemote( net::HttpRequestHeaders::kPostMethod); } }; TEST_F(PostBeaconRequestDataTest, SendBytesWithCorsSafelistedContentType) { - auto beacon_remote = CreateBeaconAndPassRemote(); + auto& beacon_remote = CreateBeaconAndPassRemote(); auto body = CreateRequestBody("data"); beacon_remote->SetRequestData(body, "text/plain"); @@ -469,7 +489,7 @@ TEST_F(PostBeaconRequestDataTest, SendBytesWithCorsSafelistedContentType) { } TEST_F(PostBeaconRequestDataTest, SendBytesWithEmptyContentType) { - auto beacon_remote = CreateBeaconAndPassRemote(); + auto& beacon_remote = CreateBeaconAndPassRemote(); auto body = CreateRequestBody("data"); beacon_remote->SetRequestData(body, ""); @@ -480,7 +500,7 @@ TEST_F(PostBeaconRequestDataTest, SendBytesWithEmptyContentType) { } TEST_F(PostBeaconRequestDataTest, SendBlobWithCorsSafelistedContentType) { - auto beacon_remote = CreateBeaconAndPassRemote(); + auto& beacon_remote = CreateBeaconAndPassRemote(); auto body = CreateFileRequestBody(); beacon_remote->SetRequestData(body, "text/plain"); @@ -491,7 +511,7 @@ TEST_F(PostBeaconRequestDataTest, SendBlobWithCorsSafelistedContentType) { } TEST_F(PostBeaconRequestDataTest, SendBlobWithEmptyContentType) { - auto beacon_remote = CreateBeaconAndPassRemote(); + auto& beacon_remote = CreateBeaconAndPassRemote(); auto body = CreateFileRequestBody(); beacon_remote->SetRequestData(body, ""); @@ -502,7 +522,7 @@ TEST_F(PostBeaconRequestDataTest, SendBlobWithEmptyContentType) { } TEST_F(PostBeaconRequestDataTest, SendBlobWithNonCorsSafelistedContentType) { - auto beacon_remote = CreateBeaconAndPassRemote(); + auto& beacon_remote = CreateBeaconAndPassRemote(); auto body = CreateFileRequestBody(); beacon_remote->SetRequestData(body, "application/unsafe"); diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 3769cb336f0328..377da2e9d2262b 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -8737,6 +8737,17 @@ bool RenderFrameHostImpl::ShouldDispatchPagehideAndVisibilitychangeDuringCommit( return true; } +void RenderFrameHostImpl::SendAllPendingBeaconsOnNavigation() { + if (auto* pending_beacon_host = + PendingBeaconHost::GetForCurrentDocument(this)) { + pending_beacon_host->SendAllOnNavigation(); + } + // TODO(crbug.com/1293679): Address FencedFrame. + for (auto& child : children_) { + child->current_frame_host()->SendAllPendingBeaconsOnNavigation(); + } +} + void RenderFrameHostImpl::CommitNavigation( NavigationRequest* navigation_request, blink::mojom::CommonNavigationParamsPtr common_params, diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h index 1f7a7bdd29011b..c6e0528a47f13f 100644 --- a/content/browser/renderer_host/render_frame_host_impl.h +++ b/content/browser/renderer_host/render_frame_host_impl.h @@ -2550,6 +2550,12 @@ class CONTENT_EXPORT RenderFrameHostImpl const storage::BucketInfo& bucket, mojo::PendingReceiver receiver) override; + // Sends out all pending beacons held by this document and all its child + // documents. + // + // This method must be called when navigating away from the current document. + void SendAllPendingBeaconsOnNavigation(); + protected: friend class RenderFrameHostFactory; diff --git a/content/browser/renderer_host/render_frame_host_manager.cc b/content/browser/renderer_host/render_frame_host_manager.cc index e6b28593b18fd4..2bbd6da56caee6 100644 --- a/content/browser/renderer_host/render_frame_host_manager.cc +++ b/content/browser/renderer_host/render_frame_host_manager.cc @@ -769,6 +769,11 @@ void RenderFrameHostManager::UnloadOldFrame( // RenderFrameHost should not be trying to commit a navigation. old_render_frame_host->ResetNavigationRequests(); + if (base::FeatureList::IsEnabled(blink::features::kPendingBeaconAPI) && + blink::features::kPendingBeaconAPIForcesSendingOnNavigation.Get()) { + old_render_frame_host->SendAllPendingBeaconsOnNavigation(); + } + NavigationEntryImpl* last_committed_entry = GetNavigationController().GetLastCommittedEntry(); BackForwardCacheMetrics* old_page_back_forward_cache_metrics = @@ -959,7 +964,7 @@ bool RenderFrameHostManager::HasPendingCommitForCrossDocumentNavigation() return true; if (speculative_render_frame_host_) { return speculative_render_frame_host_ - ->HasPendingCommitForCrossDocumentNavigation(); + ->HasPendingCommitForCrossDocumentNavigation(); } return false; } diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc index 2edd1304e89479..53f598beb1e387 100644 --- a/third_party/blink/common/features.cc +++ b/third_party/blink/common/features.cc @@ -1356,6 +1356,8 @@ const base::Feature kPendingBeaconAPI{"PendingBeaconAPI", base::FEATURE_DISABLED_BY_DEFAULT}; const base::FeatureParam kPendingBeaconAPIRequiresOriginTrial = { &kPendingBeaconAPI, "requires_origin_trial", false}; +const base::FeatureParam kPendingBeaconAPIForcesSendingOnNavigation = { + &blink::features::kPendingBeaconAPI, "send_on_navigation", true}; #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID) const base::Feature kPrefetchFontLookupTables{ diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h index e298ae1c18ad1b..c95ea8ea52500d 100644 --- a/third_party/blink/public/common/features.h +++ b/third_party/blink/public/common/features.h @@ -756,6 +756,11 @@ BLINK_COMMON_EXPORT extern const base::Feature kPendingBeaconAPI; // both in Chromium & in Blink. BLINK_COMMON_EXPORT extern const base::FeatureParam kPendingBeaconAPIRequiresOriginTrial; +// Allows control to decide whether to forced sending out beacons on navigating +// away a page (transitioning to dispatch pagehide event). +// Details in https://github.com/WICG/unload-beacon/issues/30 +BLINK_COMMON_EXPORT extern const base::FeatureParam + kPendingBeaconAPIForcesSendingOnNavigation; #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID) // If enabled, font lookup tables will be prefetched on renderer startup. diff --git a/third_party/blink/public/mojom/frame/pending_beacon.mojom b/third_party/blink/public/mojom/frame/pending_beacon.mojom index 2e038bad5c2f64..852e9373a26fca 100644 --- a/third_party/blink/public/mojom/frame/pending_beacon.mojom +++ b/third_party/blink/public/mojom/frame/pending_beacon.mojom @@ -15,6 +15,12 @@ enum BeaconMethod { }; // Interface for creating browser-side pending beacon objects. +// +// There is one instance of this interface per RenderFrameHost in the browser +// process. +// +// All methods are called by renderer. +// // API explainer here: // https://github.com/WICG/unload-beacon/blob/main/README.md interface PendingBeaconHost { @@ -30,7 +36,12 @@ interface PendingBeaconHost { }; -// Interface for configuring and acting on pending beacons. +// Interface for configuring and acting on a pending beacon in the browser. +// +// A pending beacon in the renderer process uses this interface to communicate +// with its counterpart in the browser process. +// +// All methods are called by renderer. interface PendingBeacon { // Deactivates the pending beacon. After this call it will not be sent. diff --git a/third_party/blink/renderer/core/frame/build.gni b/third_party/blink/renderer/core/frame/build.gni index 380997b3caf502..7c35ea6b1e2dad 100644 --- a/third_party/blink/renderer/core/frame/build.gni +++ b/third_party/blink/renderer/core/frame/build.gni @@ -286,6 +286,7 @@ blink_core_tests_frame = [ "mhtml_loading_test.cc", "performance_monitor_test.cc", "pending_beacon_dispatcher_test.cc", + "pending_beacon_test.cc", "policy_container_test.cc", "report_test.cc", "reporting_context_test.cc", diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc index ca10e027bd4f9f..e5592f3b24007c 100644 --- a/third_party/blink/renderer/core/frame/local_dom_window.cc +++ b/third_party/blink/renderer/core/frame/local_dom_window.cc @@ -93,6 +93,7 @@ #include "third_party/blink/renderer/core/frame/local_frame_client.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/frame/navigator.h" +#include "third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h" #include "third_party/blink/renderer/core/frame/permissions_policy_violation_report_body.h" #include "third_party/blink/renderer/core/frame/report.h" #include "third_party/blink/renderer/core/frame/reporting_context.h" @@ -836,6 +837,14 @@ void LocalDOMWindow::DispatchPagehideEvent( // TODO(crbug.com/1119291): Investigate whether this is possible or not. return; } + + if (base::FeatureList::IsEnabled(features::kPendingBeaconAPI)) { + if (auto* dispatcher = + PendingBeaconDispatcher::From(*GetExecutionContext())) { + dispatcher->OnDispatchPagehide(); + } + } + DispatchEvent( *PageTransitionEvent::Create(event_type_names::kPagehide, persistence), document_.Get()); diff --git a/third_party/blink/renderer/core/frame/pending_beacon.cc b/third_party/blink/renderer/core/frame/pending_beacon.cc index 3f784095a1c06d..56ba3f3fc6837e 100644 --- a/third_party/blink/renderer/core/frame/pending_beacon.cc +++ b/third_party/blink/renderer/core/frame/pending_beacon.cc @@ -86,9 +86,7 @@ void PendingBeacon::deactivate() { remote_->Deactivate(); pending_ = false; - auto* dispatcher = PendingBeaconDispatcher::From(*ec_); - DCHECK(dispatcher); - dispatcher->Unregister(this); + UnregisterFromDispatcher(); } } @@ -97,9 +95,7 @@ void PendingBeacon::sendNow() { remote_->SendNow(); pending_ = false; - auto* dispatcher = PendingBeaconDispatcher::From(*ec_); - DCHECK(dispatcher); - dispatcher->Unregister(this); + UnregisterFromDispatcher(); } } diff --git a/third_party/blink/renderer/core/frame/pending_beacon.h b/third_party/blink/renderer/core/frame/pending_beacon.h index eab1d0e0a1aaee..109cc44a1ac261 100644 --- a/third_party/blink/renderer/core/frame/pending_beacon.h +++ b/third_party/blink/renderer/core/frame/pending_beacon.h @@ -58,6 +58,9 @@ class CORE_EXPORT PendingBeacon // `PendingBeaconDispatcher::PendingBeacon` implementation. base::TimeDelta GetBackgroundTimeout() const override; void Send() override; + bool IsPending() const override { return pending_; } + void MarkNotPending() override { pending_ = false; } + ExecutionContext* GetExecutionContext() override { return ec_; } protected: explicit PendingBeacon(ExecutionContext* context, @@ -74,10 +77,13 @@ class CORE_EXPORT PendingBeacon // A convenient method to return a TaskRunner which is able to keep working // even if the JS context is frozen. scoped_refptr GetTaskRunner(); + // Triggered by `timeout_timer_`. void TimeoutTimerFired(TimerBase*); Member ec_; + // Connects to a PendingBeacon in the browser process. HeapMojoRemote remote_; + String url_; const String method_; base::TimeDelta background_timeout_; diff --git a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.cc b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.cc index aef913ae85059c..775dbaafcafa53 100644 --- a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.cc +++ b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.cc @@ -36,6 +36,14 @@ struct ReverseBeaconTimeoutSorter { } // namespace +void PendingBeaconDispatcher::PendingBeacon::UnregisterFromDispatcher() { + auto* ec = GetExecutionContext(); + DCHECK(ec); + auto* dispatcher = PendingBeaconDispatcher::From(*ec); + DCHECK(dispatcher); + dispatcher->Unregister(this); +} + // static const char PendingBeaconDispatcher::kSupplementName[] = "PendingBeaconDispatcher"; @@ -278,4 +286,35 @@ void PendingBeaconDispatcher::Trace(Visitor* visitor) const { visitor->Trace(background_timeout_descending_beacons_); } +bool PendingBeaconDispatcher::HasPendingBeaconForTesting( + PendingBeacon* pending_beacon) const { + return pending_beacons_.Contains(pending_beacon); +} + +void PendingBeaconDispatcher::OnDispatchPagehide() { + if (!features::kPendingBeaconAPIForcesSendingOnNavigation.Get()) { + return; + } + + // At this point, the renderer can assume that all beacons on this document + // have (or will have) been sent out by browsers. The only work left is to + // update all beacons pending state such that they cannot be updated anymore. + // + // This is to mitigate potential privacy issue that when network changes + // after users think they have left a page, beacons queued in that page + // still exist and get sent through the new network, which leaks navigation + // history to the new network. + // See https://github.com/WICG/unload-beacon/issues/30. + // + // Note that the pagehide event might be dispatched a bit earlier than when + // beacons get sents by browser in same-site navigation. + + for (auto& pending_beacon : pending_beacons_) { + if (pending_beacon->IsPending()) { + pending_beacon->MarkNotPending(); + } + } + pending_beacons_.clear(); +} + } // namespace blink diff --git a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h index f6e69596ecb0c6..5539ebc44a2663 100644 --- a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h +++ b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h @@ -5,6 +5,7 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_PENDING_BEACON_DISPATCHER_H_ #define THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_PENDING_BEACON_DISPATCHER_H_ +#include "base/gtest_prod_util.h" #include "base/time/time.h" #include "base/types/pass_key.h" #include "third_party/blink/public/mojom/frame/pending_beacon.mojom-blink.h" @@ -79,10 +80,32 @@ class CORE_EXPORT PendingBeaconDispatcher // Implementation should ensure the returned TimeDelta is not negative. virtual base::TimeDelta GetBackgroundTimeout() const = 0; // Triggers beacon sending action. - // Implementation should also transitions this beacon into non-pending - // state. and call `PendingBeaconDispatcher::Unregister()` to unregister - // itself from further scheduling. + // + // The sending action may not be triggered if it decides not to do so. + // If triggered, implementation should also transitions this beacon into + // non-pending state, and call `PendingBeaconDispatcher::Unregister()` to + // unregister itself from further scheduling. + // If not triggered, the dispatcher will schedule to send this next time as + // long as this is still registered. virtual void Send() = 0; + + virtual bool IsPending() const = 0; + virtual void MarkNotPending() = 0; + // Provides ExecutionContext where this beacon is created. + virtual ExecutionContext* GetExecutionContext() = 0; + + protected: + // Unregisters this beacon from the PendingBeaconDispatcher associated with + // `GetExecutionContext()`. + // + // Calling this method will reduce the lifetime of this instance back to the + // lifetime of the corresponding JS object, i.e. it won't be extended by the + // PendingBeaconDispatcher anymore. + // + // After this call, all existing timers, either in this PendingBeacon or in + // PendingBeaconDispatcher, are not cancelled, but will be no-op when their + // callbacks are triggered. + void UnregisterFromDispatcher(); }; static const char kSupplementName[]; @@ -139,6 +162,16 @@ class CORE_EXPORT PendingBeaconDispatcher // `PageVisibilityObserver` implementation. void PageVisibilityChanged() override; + // Handles pagehide event. + // + // The browser will force sending out all beacons on navigating to a new page, + // i.e. on pagehide event. Whether or not the old page is put into + // BackForwardCache is not important. + // + // This method asks all owned `pending_beacons_` to update their state to + // non-pending and unregisters them from this dispatcher. + void OnDispatchPagehide(); + private: // Schedules a series of tasks to dispatch pending beacons according to // their `PendingBeacon::GetBackgroundTimeout()`. @@ -207,6 +240,24 @@ class CORE_EXPORT PendingBeaconDispatcher // // It is canceled when `CancelDispatchBeacons()` is called. TaskHandle task_handle_; + + // For testing: + bool HasPendingBeaconForTesting(PendingBeacon* pending_beacon) const; + FRIEND_TEST_ALL_PREFIXES(PendingBeaconDispatcherBasicBeaconsTest, + DispatchBeaconsOnBackgroundTimeout); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconDispatcherBackgroundTimeoutBundledTest, + DispatchOrderedBeacons); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconDispatcherBackgroundTimeoutBundledTest, + DispatchReversedBeacons); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconDispatcherBackgroundTimeoutBundledTest, + DispatchDuplicatedBeacons); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconDispatcherOnPagehideTest, + OnPagehideUpdateAndUnregisterAllBeacons); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconCreateTest, Create); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconSendTest, Send); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconSendTest, SendNow); + FRIEND_TEST_ALL_PREFIXES(PendingBeaconSendTest, + SetNonPendingAfterTimeoutTimerStart); }; } // namespace blink diff --git a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher_test.cc b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher_test.cc index a28bb098d1af4f..c3cbc3e1e5eeea 100644 --- a/third_party/blink/renderer/core/frame/pending_beacon_dispatcher_test.cc +++ b/third_party/blink/renderer/core/frame/pending_beacon_dispatcher_test.cc @@ -6,9 +6,12 @@ #include #include +#include "base/strings/strcat.h" #include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" #include "base/time/time.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/features.h" #include "third_party/blink/public/mojom/frame/pending_beacon.mojom-blink-forward.h" #include "third_party/blink/public/mojom/frame/pending_beacon.mojom-blink.h" #include "third_party/blink/public/mojom/page/page_visibility_state.mojom.h" @@ -33,26 +36,26 @@ using ::testing::UnorderedElementsAre; class MockPendingBeacon : public GarbageCollected, public PendingBeaconDispatcher::PendingBeacon { public: + using OnSendCallbackType = base::RepeatingCallback; + MockPendingBeacon(ExecutionContext* ec, int id, base::TimeDelta background_timeout, - base::RepeatingCallback on_send) - : remote_(ec), + OnSendCallbackType on_send) + : ec_(ec), + remote_(ec), id_(id), background_timeout_(background_timeout), on_send_(on_send) { auto task_runner = ec->GetTaskRunner(PendingBeaconDispatcher::kTaskType); - mojo::PendingReceiver unused_receiver = + mojo::PendingReceiver receiver = remote_.BindNewPipeAndPassReceiver(task_runner); auto& dispatcher = PendingBeaconDispatcher::FromOrAttachTo(*ec); - dispatcher.CreateHostBeacon(this, std::move(unused_receiver), url_, - method_); + dispatcher.CreateHostBeacon(this, std::move(receiver), url_, method_); } - MockPendingBeacon(ExecutionContext* ec, - int id, - base::RepeatingCallback on_send) + MockPendingBeacon(ExecutionContext* ec, int id, OnSendCallbackType on_send) : MockPendingBeacon(ec, id, base::Milliseconds(-1), on_send) {} // Not copyable or movable @@ -60,25 +63,38 @@ class MockPendingBeacon : public GarbageCollected, MockPendingBeacon& operator=(const MockPendingBeacon&) = delete; virtual ~MockPendingBeacon() = default; - void Trace(Visitor* visitor) const override { visitor->Trace(remote_); } + void Trace(Visitor* visitor) const override { + visitor->Trace(ec_); + visitor->Trace(remote_); + } // PendingBeaconDispatcher::Beacon Implementation. base::TimeDelta GetBackgroundTimeout() const override { return background_timeout_; } - void Send() override { on_send_.Run(id_); } + void Send() override { + on_send_.Run(id_); + PendingBeaconDispatcher::From(*ec_)->Unregister(this); + } + ExecutionContext* GetExecutionContext() override { return ec_; } + bool IsPending() const override { return is_pending_; } + void MarkNotPending() override { is_pending_ = false; } private: const KURL url_ = KURL("/"); const mojom::blink::BeaconMethod method_ = mojom::blink::BeaconMethod::kGet; + Member ec_; HeapMojoRemote remote_; const int id_; const base::TimeDelta background_timeout_; base::RepeatingCallback on_send_; + bool is_pending_ = true; }; class PendingBeaconDispatcherTestBase : public ::testing::Test { protected: + using IdToTimeouts = std::vector>; + void TriggerDispatchOnBackgroundTimeout(V8TestingScope& scope) { auto* ec = scope.GetExecutionContext(); // Ensures that a dispatcher is attached to `ec`. @@ -86,6 +102,19 @@ class PendingBeaconDispatcherTestBase : public ::testing::Test { scope.GetPage().SetVisibilityState( blink::mojom::PageVisibilityState::kHidden, /*is_initial_state=*/false); } + + HeapVector> CreateBeacons( + V8TestingScope& v8_scope, + const IdToTimeouts& id_to_timeouts, + MockPendingBeacon::OnSendCallbackType callback) { + HeapVector> beacons; + auto* ec = v8_scope.GetExecutionContext(); + for (const auto& id_to_timeout : id_to_timeouts) { + beacons.push_back(MakeGarbageCollected( + ec, id_to_timeout.first, id_to_timeout.second, callback)); + } + return beacons; + } }; struct BeaconIdToTimeoutsTestType { @@ -146,15 +175,11 @@ TEST_P(PendingBeaconDispatcherBasicBeaconsTest, std::vector beacons_sent_order; V8TestingScope scope; - auto* ec = scope.GetExecutionContext(); - HeapVector> beacons; - for (const auto& id_to_timeout : id_to_timeouts) { - beacons.push_back(MakeGarbageCollected( - ec, id_to_timeout.first, id_to_timeout.second, - base::BindLambdaForTesting([&beacons_sent_order](int id) { - beacons_sent_order.push_back(id); - }))); - } + auto beacons = + CreateBeacons(scope, id_to_timeouts, + base::BindLambdaForTesting([&beacons_sent_order](int id) { + beacons_sent_order.push_back(id); + })); TriggerDispatchOnBackgroundTimeout(scope); while (beacons_sent_order.size() < id_to_timeouts.size()) { @@ -162,6 +187,10 @@ TEST_P(PendingBeaconDispatcherBasicBeaconsTest, } EXPECT_THAT(beacons_sent_order, testing::ContainerEq(GetParam().expected)); + for (const auto& beacon : beacons) { + EXPECT_FALSE(PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); + } } // Tests to cover the beacon bundling behavior on backgroundTimeout. @@ -181,15 +210,11 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, std::vector beacons_sent_order; V8TestingScope scope; - auto* ec = scope.GetExecutionContext(); - HeapVector> beacons; - for (const auto& id_to_timeout : id_to_timeouts) { - beacons.push_back(MakeGarbageCollected( - ec, id_to_timeout.first, id_to_timeout.second, - base::BindLambdaForTesting([&beacons_sent_order](int id) { - beacons_sent_order.push_back(id); - }))); - } + auto beacons = + CreateBeacons(scope, id_to_timeouts, + base::BindLambdaForTesting([&beacons_sent_order](int id) { + beacons_sent_order.push_back(id); + })); TriggerDispatchOnBackgroundTimeout(scope); while (beacons_sent_order.size() < id_to_timeouts.size()) { @@ -212,6 +237,10 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, EXPECT_THAT(std::vector(beacons_sent_order.begin() + 10, beacons_sent_order.begin() + 12), UnorderedElementsAre(11, 12)); + for (const auto& beacon : beacons) { + EXPECT_FALSE(PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); + } } TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, @@ -227,15 +256,11 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, std::vector beacons_sent_order; V8TestingScope scope; - auto* ec = scope.GetExecutionContext(); - HeapVector> beacons; - for (const auto& id_to_timeout : id_to_timeouts) { - beacons.push_back(MakeGarbageCollected( - ec, id_to_timeout.first, id_to_timeout.second, - base::BindLambdaForTesting([&beacons_sent_order](int id) { - beacons_sent_order.push_back(id); - }))); - } + auto beacons = + CreateBeacons(scope, id_to_timeouts, + base::BindLambdaForTesting([&beacons_sent_order](int id) { + beacons_sent_order.push_back(id); + })); TriggerDispatchOnBackgroundTimeout(scope); while (beacons_sent_order.size() < id_to_timeouts.size()) { @@ -258,6 +283,10 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, EXPECT_THAT(std::vector(beacons_sent_order.begin() + 10, beacons_sent_order.begin() + 12), UnorderedElementsAre(1, 2)); + for (const auto& beacon : beacons) { + EXPECT_FALSE(PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); + } } TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, @@ -271,15 +300,11 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, std::vector beacons_sent_order; V8TestingScope scope; - auto* ec = scope.GetExecutionContext(); - HeapVector> beacons; - for (const auto& id_to_timeout : id_to_timeouts) { - beacons.push_back(MakeGarbageCollected( - ec, id_to_timeout.first, id_to_timeout.second, - base::BindLambdaForTesting([&beacons_sent_order](int id) { - beacons_sent_order.push_back(id); - }))); - } + auto beacons = + CreateBeacons(scope, id_to_timeouts, + base::BindLambdaForTesting([&beacons_sent_order](int id) { + beacons_sent_order.push_back(id); + })); TriggerDispatchOnBackgroundTimeout(scope); while (beacons_sent_order.size() < id_to_timeouts.size()) { @@ -294,6 +319,59 @@ TEST_F(PendingBeaconDispatcherBackgroundTimeoutBundledTest, EXPECT_THAT(std::vector(beacons_sent_order.begin() + 2, beacons_sent_order.begin() + 7), UnorderedElementsAre(3, 4, 5, 6, 7)); + for (const auto& beacon : beacons) { + EXPECT_FALSE(PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); + } +} + +class PendingBeaconDispatcherOnPagehideTest + : public PendingBeaconDispatcherTestBase { + void SetUp() override { + const std::vector + enabled_features = {{blink::features::kPendingBeaconAPI, + {{"send_on_navigation", "true"}}}}; + feature_list_.InitWithFeaturesAndParameters(enabled_features, {}); + PendingBeaconDispatcherTestBase::SetUp(); + } + + private: + base::test::ScopedFeatureList feature_list_; +}; + +TEST_F(PendingBeaconDispatcherOnPagehideTest, + OnPagehideUpdateAndUnregisterAllBeacons) { + const std::vector> id_to_timeouts = { + {1, base::Milliseconds(0)}, {2, base::Milliseconds(0)}, + {3, base::Milliseconds(100)}, {4, base::Milliseconds(100)}, + {5, base::Milliseconds(100)}, {6, base::Milliseconds(101)}, + {7, base::Milliseconds(101)}, + }; + std::vector beacons_sent_order; + + V8TestingScope scope; + auto beacons = + CreateBeacons(scope, id_to_timeouts, + base::BindLambdaForTesting([&beacons_sent_order](int id) { + beacons_sent_order.push_back(id); + })); + for (const auto& beacon : beacons) { + EXPECT_TRUE(beacon->IsPending()); + } + + PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->OnDispatchPagehide(); + test::RunPendingTasks(); + + // On page hide, all beacons should be marked as non-pending. However, none + // should be sent directly by the renderer; the browser is responsible for + // this. + EXPECT_THAT(beacons_sent_order, IsEmpty()); + for (const auto& beacon : beacons) { + EXPECT_FALSE(beacon->IsPending()); + EXPECT_FALSE(PendingBeaconDispatcher::From(*scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); + } } } // namespace blink diff --git a/third_party/blink/renderer/core/frame/pending_beacon_test.cc b/third_party/blink/renderer/core/frame/pending_beacon_test.cc new file mode 100644 index 00000000000000..643281278226e0 --- /dev/null +++ b/third_party/blink/renderer/core/frame/pending_beacon_test.cc @@ -0,0 +1,238 @@ +// Copyright 2022 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "third_party/blink/renderer/core/frame/pending_beacon.h" + +#include +#include + +#include "base/strings/strcat.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_pending_beacon_options.h" +#include "third_party/blink/renderer/core/execution_context/execution_context.h" +#include "third_party/blink/renderer/core/frame/pending_beacon_dispatcher.h" +#include "third_party/blink/renderer/core/frame/pending_get_beacon.h" +#include "third_party/blink/renderer/core/frame/pending_post_beacon.h" +#include "third_party/blink/renderer/platform/network/http_names.h" +#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" +#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" + +namespace blink { + +class PendingBeaconTestBase : public ::testing::Test { + protected: + PendingBeacon* CreatePendingBeacon(V8TestingScope& v8_scope, + mojom::blink::BeaconMethod method, + const WTF::String& url, + PendingBeaconOptions* options) const { + auto* ec = v8_scope.GetExecutionContext(); + if (method == mojom::blink::BeaconMethod::kGet) { + return PendingGetBeacon::Create(ec, url, options); + } else { + return PendingPostBeacon::Create(ec, url, options); + } + } + PendingBeacon* CreatePendingBeacon(V8TestingScope& v8_scope, + mojom::blink::BeaconMethod method, + const WTF::String& url) const { + return CreatePendingBeacon(v8_scope, method, url, + PendingBeaconOptions::Create()); + } + PendingBeacon* CreatePendingBeacon(V8TestingScope& v8_scope, + mojom::blink::BeaconMethod method) const { + return CreatePendingBeacon(v8_scope, method, GetTargetURL(), + PendingBeaconOptions::Create()); + } + + static const WTF::String& GetTargetURL() { + DEFINE_STATIC_LOCAL(const AtomicString, kTargetURL, + ("/pending_beacon/send")); + return kTargetURL; + } +}; + +struct BeaconMethodTestType { + const char* name; + const mojom::blink::BeaconMethod method; + + WTF::AtomicString GetMethodString() const { + switch (method) { + case mojom::blink::BeaconMethod::kGet: + return WTF::AtomicString("GET"); + case mojom::blink::BeaconMethod::kPost: + return WTF::AtomicString("POST"); + } + CHECK(false) << "Unsupported beacon method"; + } + + static const char* TestParamInfoToName( + const ::testing::TestParamInfo& info) { + return info.param.name; + } +}; +constexpr BeaconMethodTestType kPendingGetBeaconTestCase{ + "PendingGetBeacon", mojom::blink::BeaconMethod::kGet}; +constexpr BeaconMethodTestType kPendingPostBeaconTestCase{ + "PendingPostBeacon", mojom::blink::BeaconMethod::kPost}; + +class PendingBeaconCreateTest + : public PendingBeaconTestBase, + public ::testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(All, + PendingBeaconCreateTest, + ::testing::Values(kPendingGetBeaconTestCase, + kPendingPostBeaconTestCase), + BeaconMethodTestType::TestParamInfoToName); + +TEST_P(PendingBeaconCreateTest, Create) { + V8TestingScope v8_scope; + const auto& method = GetParam().method; + const auto& method_str = GetParam().GetMethodString(); + + auto* beacon = CreatePendingBeacon(v8_scope, method); + + EXPECT_EQ(beacon->url(), GetTargetURL()); + ASSERT_EQ(beacon->method(), method_str); + ASSERT_EQ(beacon->timeout(), -1); + ASSERT_EQ(beacon->backgroundTimeout(), -1); + ASSERT_TRUE(beacon->pending()); + ASSERT_TRUE(beacon->IsPending()); + ASSERT_TRUE(PendingBeaconDispatcher::From(*v8_scope.GetExecutionContext()) + ->HasPendingBeaconForTesting(beacon)); +} + +class PendingBeaconURLTest + : public PendingBeaconTestBase, + public ::testing::WithParamInterface { + protected: + struct BeaconURLTestType { + std::string name; + std::string url_; + WTF::String GetURL() const { + return url_ == "null" ? WTF::String() : WTF::String(url_); + } + }; +}; + +INSTANTIATE_TEST_SUITE_P(All, + PendingBeaconURLTest, + ::testing::Values(kPendingGetBeaconTestCase, + kPendingPostBeaconTestCase), + BeaconMethodTestType::TestParamInfoToName); + +TEST_P(PendingBeaconURLTest, CreateWithURL) { + const std::vector test_cases = { + {"EmptyURL", ""}, + {"RootURL", "/"}, + {"RelativePathURL", "/path/to/page"}, + {"NullURL", "null"}, + {"RandomPhraseURL", "test"}, + {"LocalHostURL", "localhost"}, + {"AddressURL", "127.0.0.1"}, + {"HTTPURL", "http://example.com"}, + {"HTTPSURL", "https://example.com"}, + }; + const auto& method = GetParam().method; + V8TestingScope v8_scope; + + for (const auto& test_case : test_cases) { + const auto& url = test_case.GetURL(); + + auto* beacon = CreatePendingBeacon(v8_scope, method, url); + + EXPECT_EQ(beacon->url(), url) << test_case.name; + } +} + +class PendingBeaconBasicOperationsTest + : public PendingBeaconTestBase, + public ::testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(All, + PendingBeaconBasicOperationsTest, + ::testing::Values(kPendingGetBeaconTestCase, + kPendingPostBeaconTestCase), + BeaconMethodTestType::TestParamInfoToName); + +TEST_P(PendingBeaconBasicOperationsTest, MarkNotPending) { + V8TestingScope v8_scope; + const auto& method = GetParam().method; + + auto* beacon = CreatePendingBeacon(v8_scope, method); + ASSERT_TRUE(beacon->pending()); + ASSERT_TRUE(beacon->IsPending()); + + beacon->MarkNotPending(); + + ASSERT_FALSE(beacon->pending()); + ASSERT_FALSE(beacon->IsPending()); +} + +class PendingBeaconSendTest + : public PendingBeaconTestBase, + public ::testing::WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(All, + PendingBeaconSendTest, + ::testing::Values(kPendingGetBeaconTestCase, + kPendingPostBeaconTestCase), + BeaconMethodTestType::TestParamInfoToName); + +TEST_P(PendingBeaconSendTest, Send) { + V8TestingScope v8_scope; + const auto& method = GetParam().method; + auto* beacon = CreatePendingBeacon(v8_scope, method); + auto* dispatcher = + PendingBeaconDispatcher::From(*v8_scope.GetExecutionContext()); + ASSERT_TRUE(dispatcher); + ASSERT_TRUE(dispatcher->HasPendingBeaconForTesting(beacon)); + ASSERT_TRUE(beacon->pending()); + EXPECT_TRUE(beacon->IsPending()); + + beacon->Send(); + + EXPECT_FALSE(dispatcher->HasPendingBeaconForTesting(beacon)); + EXPECT_FALSE(beacon->pending()); + EXPECT_FALSE(beacon->IsPending()); +} + +TEST_P(PendingBeaconSendTest, SendNow) { + V8TestingScope v8_scope; + const auto& method = GetParam().method; + auto* beacon = CreatePendingBeacon(v8_scope, method); + auto* dispatcher = + PendingBeaconDispatcher::From(*v8_scope.GetExecutionContext()); + ASSERT_TRUE(dispatcher); + ASSERT_TRUE(dispatcher->HasPendingBeaconForTesting(beacon)); + ASSERT_TRUE(beacon->pending()); + EXPECT_TRUE(beacon->IsPending()); + + beacon->sendNow(); + + EXPECT_FALSE(dispatcher->HasPendingBeaconForTesting(beacon)); + EXPECT_FALSE(beacon->pending()); + EXPECT_FALSE(beacon->IsPending()); +} + +TEST_P(PendingBeaconSendTest, SetNonPendingAfterTimeoutTimerStart) { + V8TestingScope v8_scope; + const auto& method = GetParam().method; + auto* beacon = CreatePendingBeacon(v8_scope, method); + auto* dispatcher = + PendingBeaconDispatcher::From(*v8_scope.GetExecutionContext()); + ASSERT_TRUE(dispatcher); + beacon->setTimeout(60000); // 60s such that it can't be reached in this test. + ASSERT_TRUE(dispatcher->HasPendingBeaconForTesting(beacon)); + ASSERT_TRUE(beacon->pending()); + + beacon->MarkNotPending(); + + EXPECT_FALSE(beacon->pending()); + // Unregistering is handled by dispatcher. +} + +} // namespace blink