CVE-2026-79018
Overview
Background
- Device Posture API
- a web API (
navigator.devicePosture) that tells a page whether a foldable device iscontinuousorfolded, backed in the browser byDevicePostureProviderImpl, which pushesOnPostureChangedto eachblink::mojom::DevicePostureClientregistered for aWebContents. - `DevicePosturePlatformProvider`
- the platform backend that reports the physical posture and calls
DevicePostureProviderImpl::OnDevicePostureChangedwhen the hinge state changes. - `WebContentsObserver::OnVisibilityChanged`
- a browser-side callback fired when a tab becomes
VISIBLE,HIDDEN, orOCCLUDED. Before this fix the posture provider did not observe it. - DevTools posture emulation
OverrideDevicePostureForEmulation/DisableDevicePostureOverrideForEmulationreplace the platform posture with an emulated value for a givenWebContents.
Root Cause Analysis
DevicePostureProviderImpl sent every platform posture change to every registered client, whatever the visibility of the owning WebContents. The removed code in OnDevicePostureChanged only skipped dispatch when is_posture_emulated_ was set, and the emulation override and restore paths also looped over posture_clients_ with no visibility check.
As a result, pages in background (hidden) or occluded tabs got real-time posture-change events. Every tab, from any site, that subscribed would receive the same event at the same moment, which gives a cross-tab side channel about the user’s physical handling of the device (the commit title says “Privacy Side-Channel … via Background Tabs”).
The fix makes the class a WebContentsObserver. It returns early from all three dispatch paths when web_contents()->GetVisibility() != Visibility::VISIBLE, records last_dispatched_posture_, and in the new OnVisibilityChanged sends the current posture only when the tab becomes visible again and the value has changed. Hidden pages therefore see at most one catch-up value when shown, not a timed stream of events.
WebContents is hidden or occluded and sends a single catch-up value when the tab becomes visible again.Attack Path
- Subscribe to posture
A web page subscribes to Device Posture changes, which reaches
AddListenerAndGetCurrentPostureand adds a client toposture_clients_. This is reachable from ordinary web content on a device and platform that provides a posture backend, and the bug only matters on foldable hardware. - Go to the background
The user switches tabs or covers the window, so the
WebContentsbecomesHIDDENorOCCLUDED, and the page keeps its listener. - Receive events while hidden
When the user folds or unfolds the device,
OnDevicePostureChangeddispatchedOnPostureChangedto the hidden page, because the only pre-patch guard wasis_posture_emulated_. - Correlate across tabs (inferred) Several cooperating or tracking pages in different background tabs or origins could match the timing of identical posture events to link one user’s sessions, or to infer user activity while the page was not in view.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebContentsObservercontent/browser/device_posture/device_posture_provider_impl.cc |
modified | |
forcontent/browser/device_posture/device_posture_provider_impl.cc |
modified | |
ifcontent/browser/device_posture/device_posture_provider_impl.cc |
modified | |
DevicePostureProviderImplcontent/browser/device_posture/device_posture_provider_impl.h |
modified | |
CONTENT_EXPORTcontent/browser/device_posture/device_posture_provider_impl.h |
modified | |
MockDevicePostureClientcontent/browser/device_posture/device_posture_provider_impl_unittest.cc |
modified | |
DevicePostureProviderImplTestcontent/browser/device_posture/device_posture_provider_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/device_posture/device_posture_provider_impl_unittest.cc |
modified |
Files Changed
content/browser/device_posture/device_posture_provider_impl_unittest.cccontent/browser/device_posture/device_posture_provider_impl.hcontent/browser/device_posture/device_posture_provider_impl.cccontent/test/BUILD.gn
Audit Directions
- Sensor pushes to hidden pagesReview other browser-side providers that push device or environment state to renderers (screen and window placement, display or orientation changes, pressure, ambient sensors, gamepad, idle) and check whether they gate dispatch on
WebContentsvisibility or focus. - Missing visibility observersLook for
WebContentsUserDataproviders that hold amojo::RemoteSetof clients but do not implementWebContentsObserver::OnVisibilityChanged. Each one may be sending updates to background tabs. - Emulation paths bypassing gatesWhen a real-signal path gains a visibility or permission gate, check that DevTools emulation and override paths (here
OverrideDevicePostureForEmulation) get the same gate and do not dispatch directly.
Patch
From a743780616a342356187a6693431edc81a172073 Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> Date: Tue, 07 Jul 2026 16:09:17 -0700 Subject: [PATCH] Fix Privacy Side-Channel in Device Posture API via Background Tabs This CL defers device posture change events when the WebContents is hidden or occluded. DevicePostureProviderImpl now inherits from WebContentsObserver and observes visibility changes. Updates are only dispatched when the WebContents is visible. Bug: 522803735 Change-Id: Ie5c757727cf7787c9968c816e1ff2eb50679a08c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7927408 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658356} --- diff --git a/content/browser/device_posture/device_posture_provider_impl.cc b/content/browser/device_posture/device_posture_provider_impl.cc index 6ce8c665..f592bb27 100644 --- a/content/browser/device_posture/device_posture_provider_impl.cc +++ b/content/browser/device_posture/device_posture_provider_impl.cc @@ -22,7 +22,8 @@ } DevicePostureProviderImpl::DevicePostureProviderImpl(WebContents* web_contents) - : WebContentsUserData<DevicePostureProviderImpl>(*web_contents) { + : WebContentsUserData<DevicePostureProviderImpl>(*web_contents), + WebContentsObserver(web_contents) { platform_provider_ = DevicePosturePlatformProvider::Create(web_contents); // We need to listen to disconnections so that if there is nobody interested // in posture changes we can shutdown the native backends. @@ -42,6 +43,13 @@ return platform_provider_.get(); } +blink::mojom::DevicePostureType DevicePostureProviderImpl::GetCurrentPosture() + const { + return (is_posture_emulated_ && emulated_posture_) + ? *emulated_posture_ + : platform_provider_->GetDevicePosture(); +} + void DevicePostureProviderImpl::AddListenerAndGetCurrentPosture( mojo::PendingRemote<blink::mojom::DevicePostureClient> client, AddListenerAndGetCurrentPostureCallback callback) { @@ -49,17 +57,27 @@ platform_provider_->AddObserver(this); } posture_clients_.Add(std::move(client)); - blink::mojom::DevicePostureType posture = - platform_provider_->GetDevicePosture(); + blink::mojom::DevicePostureType posture = GetCurrentPosture(); std::move(callback).Run(posture); + last_dispatched_posture_ = posture; } void DevicePostureProviderImpl::OverrideDevicePostureForEmulation( blink::mojom::DevicePostureType emulated_posture) { - // Notify the related clients about the new posture. is_posture_emulated_ = true; - for (auto& client : posture_clients_) { - client->OnPostureChanged(emulated_posture); + emulated_posture_ = emulated_posture; + // If the page is hidden, we store the emulated posture but defer dispatching + // the event until the page becomes visible again (handled in + // OnVisibilityChanged). + if (web_contents()->GetVisibility() != Visibility::VISIBLE) { + return; + } + if (!last_dispatched_posture_ || + *last_dispatched_posture_ != emulated_posture) { + for (auto& client : posture_clients_) { + client->OnPostureChanged(emulated_posture); + } + last_dispatched_posture_ = emulated_posture; } } @@ -72,21 +90,52 @@ // Restore the original posture from the platform. is_posture_emulated_ = false; - for (auto& client : posture_clients_) { - client->OnPostureChanged(platform_provider_->GetDevicePosture()); + emulated_posture_.reset(); + if (web_contents()->GetVisibility() != Visibility::VISIBLE) { + return; + } + blink::mojom::DevicePostureType posture = + platform_provider_->GetDevicePosture(); + if (!last_dispatched_posture_ || *last_dispatched_posture_ != posture) { + for (auto& client : posture_clients_) { + client->OnPostureChanged(posture); + } + last_dispatched_posture_ = posture; } } void DevicePostureProviderImpl::OnDevicePostureChanged( const blink::mojom::DevicePostureType& posture) { - // If we receive a posture change from the platform but we're emulating it we - // shouldn't notify the clients. - if (is_posture_emulated_) { + // If we receive a posture change from the platform but we're emulating it, + // or if the page is not visible, we shouldn't notify the clients. + if (is_posture_emulated_ || + web_contents()->GetVisibility() != Visibility::VISIBLE) { return; } - for (auto& client : posture_clients_) { - client->OnPostureChanged(posture); + if (!last_dispatched_posture_ || *last_dispatched_posture_ != posture) { + for (auto& client : posture_clients_) { + client->OnPostureChanged(posture); + } + last_dispatched_posture_ = posture; + } +} + +void DevicePostureProviderImpl::OnVisibilityChanged(Visibility visibility) { + if (visibility != Visibility::VISIBLE || posture_clients_.empty()) { + return; + } + + // When the tab regains visibility, dispatch the latest posture to the + // clients if it changed while the tab was hidden. + blink::mojom::DevicePostureType current_posture = GetCurrentPosture(); + + if (!last_dispatched_posture_ || + *last_dispatched_posture_ != current_posture) { + for (auto& client : posture_clients_) { + client->OnPostureChanged(current_posture); + } + last_dispatched_posture_ = current_posture; } } @@ -95,6 +144,7 @@ if (posture_clients_.empty()) { // We're not interested in receiving posture changes. platform_provider_->RemoveObserver(this); + last_dispatched_posture_.reset(); } } diff --git a/content/browser/device_posture/device_posture_provider_impl.h b/content/browser/device_posture/device_posture_provider_impl.h index ce621efe..e91786ae 100644 --- a/content/browser/device_posture/device_posture_provider_impl.h +++ b/content/browser/device_posture/device_posture_provider_impl.h @@ -7,6 +7,7 @@ #include "content/browser/device_posture/device_posture_platform_provider.h" #include "content/common/content_export.h" +#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_user_data.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" @@ -16,10 +17,11 @@ namespace content { -class DevicePostureProviderImpl final +class CONTENT_EXPORT DevicePostureProviderImpl final : public blink::mojom::DevicePostureProvider, public WebContentsUserData<DevicePostureProviderImpl>, - public DevicePosturePlatformProvider::Observer { + public DevicePosturePlatformProvider::Observer, + public WebContentsObserver { public: static DevicePostureProviderImpl* GetOrCreate(WebContents*); explicit DevicePostureProviderImpl(content::WebContents* web_contents); @@ -32,15 +34,18 @@ mojo::PendingReceiver<blink::mojom::DevicePostureProvider> receiver); DevicePosturePlatformProvider* platform_provider() const; - CONTENT_EXPORT void OverrideDevicePostureForEmulation( + void OverrideDevicePostureForEmulation( blink::mojom::DevicePostureType posture); - CONTENT_EXPORT void DisableDevicePostureOverrideForEmulation(); + void DisableDevicePostureOverrideForEmulation(); private: // DevicePostureClient implementation. void OnDevicePostureChanged( const blink::mojom::DevicePostureType& posture) override; + // WebContentsObserver implementation. + void OnVisibilityChanged(Visibility visibility) override; + // DevicePostureProvider implementation. void AddListenerAndGetCurrentPosture( mojo::PendingRemote<blink::mojom::DevicePostureClient> client, @@ -48,8 +53,12 @@ void OnRemoteDisconnect(mojo::RemoteSetElementId); + blink::mojom::DevicePostureType GetCurrentPosture() const; + std::unique_ptr<DevicePosturePlatformProvider> platform_provider_; bool is_posture_emulated_ = false; + std::optional<blink::mojom::DevicePostureType> emulated_posture_; + std::optional<blink::mojom::DevicePostureType> last_dispatched_posture_; mojo::ReceiverSet<blink::mojom::DevicePostureProvider> receivers_; mojo::RemoteSet<blink::mojom::DevicePostureClient> posture_clients_; diff --git a/content/browser/device_posture/device_posture_provider_impl_unittest.cc b/content/browser/device_posture/device_posture_provider_impl_unittest.cc new file mode 100644 index 0000000..ca934abf --- /dev/null +++ b/content/browser/device_posture/device_posture_provider_impl_unittest.cc @@ -0,0 +1,181 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/device_posture/device_posture_provider_impl.h" + +#include <optional> +#include <utility> + +#include "base/functional/bind.h" +#include "base/memory/raw_ptr.h" +#include "base/test/test_future.h" +#include "content/public/test/test_renderer_host.h" +#include "content/test/test_web_contents.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/mojom/device_posture/device_posture_provider.mojom.h" + +namespace content { + +using ::testing::_; + +class MockDevicePostureClient : public blink::mojom::DevicePostureClient { + public: + MockDevicePostureClient() = default; + ~MockDevicePostureClient() override = default; + + mojo::PendingRemote<blink::mojom::DevicePostureClient> BindAndGetRemote() { + return receiver_.BindNewPipeAndPassRemote(); + } + + // blink::mojom::DevicePostureClient: + MOCK_METHOD(void, + OnPostureChanged, + (blink::mojom::DevicePostureType posture), + (override)); + + void Flush() { receiver_.FlushForTesting(); } + + private: + mojo::Receiver<blink::mojom::DevicePostureClient> receiver_{this}; +}; + +class DevicePostureProviderImplTest : public RenderViewHostTestHarness { + public: + DevicePostureProviderImplTest() = default; + + void SetUp() override { + RenderViewHostTestHarness::SetUp(); + DevicePostureProviderImpl::GetOrCreate(web_contents()); + } + + DevicePostureProviderImpl* provider() { + return DevicePostureProviderImpl::FromWebContents(web_contents()); + } + + TestWebContents* test_web_contents() { + return static_cast<TestWebContents*>(web_contents()); + } +}; + +TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHidden) { + MockDevicePostureClient client; + + // Initially visible. + test_web_contents()->WasShown(); + + // Add listener. + base::test::TestFuture<blink::mojom::DevicePostureType> posture_future; + blink::mojom::DevicePostureProvider* posture_provider = provider(); + posture_provider->AddListenerAndGetCurrentPosture( + client.BindAndGetRemote(), posture_future.GetCallback()); + EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous); + + // Trigger posture change while visible. + DevicePosturePlatformProvider::Observer* observer = provider(); + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_1)); + observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded); + EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded); + + // Hide the web contents. + test_web_contents()->WasHidden(); + + // Trigger posture change while hidden. + EXPECT_CALL(client, OnPostureChanged(_)).Times(0); + observer->OnDevicePostureChanged( + blink::mojom::DevicePostureType::kContinuous); + client.Flush(); + + // Show the web contents again. + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_2)); + test_web_contents()->WasShown(); + EXPECT_EQ(change_future_2.Take(), + blink::mojom::DevicePostureType::kContinuous); +} + +TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileOccluded) { + MockDevicePostureClient client; + + // Initially visible. + test_web_contents()->WasShown(); + + // Add listener. + base::test::TestFuture<blink::mojom::DevicePostureType> posture_future; + blink::mojom::DevicePostureProvider* posture_provider = provider(); + posture_provider->AddListenerAndGetCurrentPosture( + client.BindAndGetRemote(), posture_future.GetCallback()); + EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous); + + // Trigger posture change while visible. + DevicePosturePlatformProvider::Observer* observer = provider(); + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_1)); + observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded); + EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded); + + // Occlude the web contents. + test_web_contents()->WasOccluded(); + + // Trigger posture change while occluded. + EXPECT_CALL(client, OnPostureChanged(_)).Times(0); + observer->OnDevicePostureChanged( + blink::mojom::DevicePostureType::kContinuous); + client.Flush(); + + // Show the web contents again. + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_2)); + test_web_contents()->WasShown(); + EXPECT_EQ(change_future_2.Take(), + blink::mojom::DevicePostureType::kContinuous); +} + +TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHiddenEmulation) { + MockDevicePostureClient client; + + // Initially visible. + test_web_contents()->WasShown(); + + // Add listener. + base::test::TestFuture<blink::mojom::DevicePostureType> posture_future; + blink::mojom::DevicePostureProvider* posture_provider = provider(); + posture_provider->AddListenerAndGetCurrentPosture( + client.BindAndGetRemote(), posture_future.GetCallback()); + EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous); + + // Enable emulation and override. + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_1)); + provider()->OverrideDevicePostureForEmulation( + blink::mojom::DevicePostureType::kFolded); + EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded); + + // Hide the web contents. + test_web_contents()->WasHidden(); + + // Override while hidden. + EXPECT_CALL(client, OnPostureChanged(_)).Times(0); + provider()->OverrideDevicePostureForEmulation( + blink::mojom::DevicePostureType::kContinuous); + client.Flush(); + + // Show the web contents again. + base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2; + EXPECT_CALL(client, OnPostureChanged(_)) + .WillOnce(base::test::InvokeFuture(change_future_2)); + test_web_contents()->WasShown(); + EXPECT_EQ(change_future_2.Take(), + blink::mojom::DevicePostureType::kContinuous); +} + +} // namespace content diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn index 131e19a..e593cfc3 100644 --- a/content/test/BUILD.gn +++ b/content/test/BUILD.gn @@ -2701,6 +2701,7 @@ "../browser/cpu_performance/cpu_performance_unittest.cc", "../browser/declarative_performance_observer/declarative_performance_observer_store_unittest.cc", ... (file diff truncated)
Regression Test / PoC
diff --git a/content/browser/device_posture/device_posture_provider_impl_unittest.cc b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
new file mode 100644
index 0000000..ca934abf
--- /dev/null
+++ b/content/browser/device_posture/device_posture_provider_impl_unittest.cc
@@ -0,0 +1,181 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/device_posture/device_posture_provider_impl.h"
+
+#include <optional>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/test_future.h"
+#include "content/public/test/test_renderer_host.h"
+#include "content/test/test_web_contents.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/device_posture/device_posture_provider.mojom.h"
+
+namespace content {
+
+using ::testing::_;
+
+class MockDevicePostureClient : public blink::mojom::DevicePostureClient {
+ public:
+ MockDevicePostureClient() = default;
+ ~MockDevicePostureClient() override = default;
+
+ mojo::PendingRemote<blink::mojom::DevicePostureClient> BindAndGetRemote() {
+ return receiver_.BindNewPipeAndPassRemote();
+ }
+
+ // blink::mojom::DevicePostureClient:
+ MOCK_METHOD(void,
+ OnPostureChanged,
+ (blink::mojom::DevicePostureType posture),
+ (override));
+
+ void Flush() { receiver_.FlushForTesting(); }
+
+ private:
+ mojo::Receiver<blink::mojom::DevicePostureClient> receiver_{this};
+};
+
+class DevicePostureProviderImplTest : public RenderViewHostTestHarness {
+ public:
+ DevicePostureProviderImplTest() = default;
+
+ void SetUp() override {
+ RenderViewHostTestHarness::SetUp();
+ DevicePostureProviderImpl::GetOrCreate(web_contents());
+ }
+
+ DevicePostureProviderImpl* provider() {
+ return DevicePostureProviderImpl::FromWebContents(web_contents());
+ }
+
+ TestWebContents* test_web_contents() {
+ return static_cast<TestWebContents*>(web_contents());
+ }
+};
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHidden) {
+ MockDevicePostureClient client;
+
+ // Initially visible.
+ test_web_contents()->WasShown();
+
+ // Add listener.
+ base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+ blink::mojom::DevicePostureProvider* posture_provider = provider();
+ posture_provider->AddListenerAndGetCurrentPosture(
+ client.BindAndGetRemote(), posture_future.GetCallback());
+ EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+ // Trigger posture change while visible.
+ DevicePosturePlatformProvider::Observer* observer = provider();
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_1));
+ observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+ EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+ // Hide the web contents.
+ test_web_contents()->WasHidden();
+
+ // Trigger posture change while hidden.
+ EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+ observer->OnDevicePostureChanged(
+ blink::mojom::DevicePostureType::kContinuous);
+ client.Flush();
+
+ // Show the web contents again.
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_2));
+ test_web_contents()->WasShown();
+ EXPECT_EQ(change_future_2.Take(),
+ blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileOccluded) {
+ MockDevicePostureClient client;
+
+ // Initially visible.
+ test_web_contents()->WasShown();
+
+ // Add listener.
+ base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+ blink::mojom::DevicePostureProvider* posture_provider = provider();
+ posture_provider->AddListenerAndGetCurrentPosture(
+ client.BindAndGetRemote(), posture_future.GetCallback());
+ EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+ // Trigger posture change while visible.
+ DevicePosturePlatformProvider::Observer* observer = provider();
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_1));
+ observer->OnDevicePostureChanged(blink::mojom::DevicePostureType::kFolded);
+ EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+ // Occlude the web contents.
+ test_web_contents()->WasOccluded();
+
+ // Trigger posture change while occluded.
+ EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+ observer->OnDevicePostureChanged(
+ blink::mojom::DevicePostureType::kContinuous);
+ client.Flush();
+
+ // Show the web contents again.
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_2));
+ test_web_contents()->WasShown();
+ EXPECT_EQ(change_future_2.Take(),
+ blink::mojom::DevicePostureType::kContinuous);
+}
+
+TEST_F(DevicePostureProviderImplTest, DeferUpdatesWhileHiddenEmulation) {
+ MockDevicePostureClient client;
+
+ // Initially visible.
+ test_web_contents()->WasShown();
+
+ // Add listener.
+ base::test::TestFuture<blink::mojom::DevicePostureType> posture_future;
+ blink::mojom::DevicePostureProvider* posture_provider = provider();
+ posture_provider->AddListenerAndGetCurrentPosture(
+ client.BindAndGetRemote(), posture_future.GetCallback());
+ EXPECT_EQ(posture_future.Get(), blink::mojom::DevicePostureType::kContinuous);
+
+ // Enable emulation and override.
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_1;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_1));
+ provider()->OverrideDevicePostureForEmulation(
+ blink::mojom::DevicePostureType::kFolded);
+ EXPECT_EQ(change_future_1.Take(), blink::mojom::DevicePostureType::kFolded);
+
+ // Hide the web contents.
+ test_web_contents()->WasHidden();
+
+ // Override while hidden.
+ EXPECT_CALL(client, OnPostureChanged(_)).Times(0);
+ provider()->OverrideDevicePostureForEmulation(
+ blink::mojom::DevicePostureType::kContinuous);
+ client.Flush();
+
+ // Show the web contents again.
+ base::test::TestFuture<blink::mojom::DevicePostureType> change_future_2;
+ EXPECT_CALL(client, OnPostureChanged(_))
+ .WillOnce(base::test::InvokeFuture(change_future_2));
+ test_web_contents()->WasShown();
+ EXPECT_EQ(change_future_2.Take(),
+ blink::mojom::DevicePostureType::kContinuous);
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 131e19a..e593cfc3 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2701,6 +2701,7 @@
"../browser/cpu_performance/cpu_performance_unittest.cc",
"../browser/declarative_performance_observer/declarative_performance_observer_store_unittest.cc",
"../browser/declarative_performance_observer/declarative_performance_observer_unittest.cc",
+ "../browser/device_posture/device_posture_provider_impl_unittest.cc",
"../browser/devtools/devtools_background_services_context_impl_unittest.cc",
"../browser/devtools/devtools_frontend_host_impl_unittest.cc",
"../browser/devtools/devtools_http_handler_unittest.cc",