| // Copyright 2018 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 "chrome/browser/resource_coordinator/local_site_characteristics_webcontents_observer.h" |
| |
| #include "base/bind.h" |
| #include "base/macros.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_data_store_factory.h" |
| #include "chrome/browser/resource_coordinator/local_site_characteristics_data_unittest_utils.h" |
| #include "chrome/browser/resource_coordinator/page_signal_receiver.h" |
| #include "chrome/browser/resource_coordinator/site_characteristics_data_store.h" |
| #include "chrome/browser/resource_coordinator/tab_manager_features.h" |
| #include "chrome/browser/resource_coordinator/time.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/favicon_url.h" |
| #include "content/public/test/mock_navigation_handle.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace resource_coordinator { |
| |
| using LoadingState = TabLoadTracker::LoadingState; |
| |
| // A mock implementation of a SiteCharacteristicsDataWriter. |
| class LenientMockDataWriter : public SiteCharacteristicsDataWriter { |
| public: |
| explicit LenientMockDataWriter(const url::Origin& origin) : origin_(origin) {} |
| ~LenientMockDataWriter() override { OnDestroy(); } |
| |
| // Mock function to be notified when this object gets destroyed. |
| MOCK_METHOD0(OnDestroy, void()); |
| |
| MOCK_METHOD0(NotifySiteLoaded, void()); |
| MOCK_METHOD0(NotifySiteUnloaded, void()); |
| MOCK_METHOD1(NotifySiteVisibilityChanged, void(TabVisibility)); |
| MOCK_METHOD0(NotifyUpdatesFaviconInBackground, void()); |
| MOCK_METHOD0(NotifyUpdatesTitleInBackground, void()); |
| MOCK_METHOD0(NotifyUsesAudioInBackground, void()); |
| MOCK_METHOD0(NotifyUsesNotificationsInBackground, void()); |
| MOCK_METHOD3(NotifyLoadTimePerformanceMeasurement, |
| void(base::TimeDelta, base::TimeDelta, uint64_t)); |
| |
| const url::Origin& Origin() const { return origin_; } |
| |
| private: |
| url::Origin origin_; |
| |
| DISALLOW_COPY_AND_ASSIGN(LenientMockDataWriter); |
| }; |
| using MockDataWriter = ::testing::StrictMock<LenientMockDataWriter>; |
| |
| // A data store that serves MockDataWriter objects. |
| class MockDataStore : public SiteCharacteristicsDataStore { |
| public: |
| MockDataStore() = default; |
| ~MockDataStore() override {} |
| |
| // SiteCharacteristicsDataStore: |
| std::unique_ptr<SiteCharacteristicsDataReader> GetReaderForOrigin( |
| const url::Origin& origin) override { |
| return nullptr; |
| } |
| std::unique_ptr<SiteCharacteristicsDataWriter> GetWriterForOrigin( |
| const url::Origin& origin, |
| TabVisibility tab_visibility) override { |
| return std::make_unique<MockDataWriter>(origin); |
| } |
| bool IsRecordingForTesting() override { return true; } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(MockDataStore); |
| }; |
| |
| std::unique_ptr<KeyedService> BuildMockDataStoreForContext( |
| content::BrowserContext* browser_context) { |
| return std::make_unique<MockDataStore>(); |
| } |
| |
| class LocalSiteCharacteristicsWebContentsObserverTest |
| : public testing::ChromeTestHarnessWithLocalDB { |
| protected: |
| LocalSiteCharacteristicsWebContentsObserverTest() |
| : scoped_set_tick_clock_for_testing_(&test_clock_) {} |
| ~LocalSiteCharacteristicsWebContentsObserverTest() override = default; |
| |
| void SetUp() override { |
| testing::ChromeTestHarnessWithLocalDB::SetUp(); |
| |
| test_clock().Advance(base::TimeDelta::FromSeconds(1)); |
| // Set the testing factory for the test browser context. |
| LocalSiteCharacteristicsDataStoreFactory::GetInstance()->SetTestingFactory( |
| browser_context(), base::BindRepeating(&BuildMockDataStoreForContext)); |
| |
| TabLoadTracker::Get()->StartTracking(web_contents()); |
| observer_ = std::make_unique<LocalSiteCharacteristicsWebContentsObserver>( |
| web_contents()); |
| observer()->SetPageSignalReceiverForTesting(&receiver_); |
| } |
| |
| void TearDown() override { |
| TabLoadTracker::Get()->StopTracking(web_contents()); |
| DeleteContents(); |
| observer_.reset(); |
| testing::ChromeTestHarnessWithLocalDB::TearDown(); |
| } |
| |
| MockDataWriter* NavigateAndReturnMockWriter(const GURL& url) { |
| content::WebContentsTester* web_contents_tester = |
| content::WebContentsTester::For(web_contents()); |
| EXPECT_TRUE(web_contents_tester); |
| web_contents_tester->NavigateAndCommit(url); |
| return static_cast<MockDataWriter*>(observer_->GetWriterForTesting()); |
| } |
| |
| const GURL kTestUrl1 = GURL("http://foo.com"); |
| const GURL kTestUrl2 = GURL("http://bar.com"); |
| |
| LocalSiteCharacteristicsWebContentsObserver* observer() { |
| return observer_.get(); |
| } |
| |
| PageNavigationIdentity GetNavIdForWebContents() { |
| return {CoordinationUnitID(), |
| receiver_.GetNavigationIDForWebContents(web_contents()), ""}; |
| } |
| |
| base::SimpleTestTickClock& test_clock() { return test_clock_; } |
| |
| private: |
| std::unique_ptr<LocalSiteCharacteristicsWebContentsObserver> observer_; |
| PageSignalReceiver receiver_; |
| base::SimpleTestTickClock test_clock_; |
| ScopedSetTickClockForTesting scoped_set_tick_clock_for_testing_; |
| |
| DISALLOW_COPY_AND_ASSIGN(LocalSiteCharacteristicsWebContentsObserverTest); |
| }; |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| NavigationEventsBasicTests) { |
| // Send a navigation event with the |committed| bit set and make sure that a |
| // writer has been created for this origin. |
| |
| EXPECT_FALSE(observer()->GetWriterForTesting()); |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| EXPECT_TRUE(mock_writer); |
| |
| auto writer_origin = observer()->GetWriterOriginForTesting(); |
| |
| EXPECT_EQ(url::Origin::Create(kTestUrl1), writer_origin); |
| |
| // A navigation to the same origin shouldn't cause caused this writer to get |
| // destroyed. |
| mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Navigate to a different origin but don't set the |committed| bit, this |
| // shouldn't affect the writer. |
| content::MockNavigationHandle navigation_handle( |
| kTestUrl2, web_contents()->GetMainFrame()); |
| observer()->DidFinishNavigation(&navigation_handle); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Set the |committed| bit and ensure that the navigation event cause the |
| // destruction of the writer. |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| mock_writer = NavigateAndReturnMockWriter(kTestUrl2); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_FALSE(writer_origin == observer()->GetWriterOriginForTesting()); |
| writer_origin = observer()->GetWriterOriginForTesting(); |
| |
| EXPECT_EQ(url::Origin::Create(kTestUrl2), mock_writer->Origin()); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| // Test that the feature usage events get forwarded to the writer when the tab |
| // is in background. |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| FeatureEventsGetForwardedWhenInBackground) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| |
| // Send dummy events to simulate the initial title/favicon update (as these |
| // are ignored). |
| observer()->DidUpdateFaviconURL({}); |
| observer()->TitleWasSet(nullptr); |
| |
| EXPECT_CALL(*mock_writer, NotifySiteLoaded()); |
| TabLoadTracker::Get()->TransitionStateForTesting(web_contents(), |
| LoadingState::LOADED); |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kForeground)); |
| EXPECT_CALL(*mock_writer, NotifySiteLoaded()); |
| web_contents()->WasShown(); |
| observer()->OnLoadingStateChange(web_contents(), |
| TabLoadTracker::LoadingState::LOADING, |
| TabLoadTracker::LoadingState::LOADED); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Ensure that no event gets forwarded if the tab is not in background. |
| observer()->DidUpdateFaviconURL({}); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| observer()->TitleWasSet(nullptr); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| observer()->OnAudioStateChanged(true); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| observer()->OnNonPersistentNotificationCreated(web_contents(), |
| GetNavIdForWebContents()); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)); |
| web_contents()->WasHidden(); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Notification usage events always get forwarded. |
| EXPECT_CALL(*mock_writer, NotifyUsesNotificationsInBackground()); |
| observer()->OnNonPersistentNotificationCreated(web_contents(), |
| GetNavIdForWebContents()); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| auto params = GetStaticSiteCharacteristicsDatabaseParams(); |
| // Title and Favicon should be ignored during the post-loading grace period. |
| observer()->DidUpdateFaviconURL({}); |
| observer()->TitleWasSet(nullptr); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| test_clock().Advance(params.title_or_favicon_change_grace_period); |
| |
| EXPECT_CALL(*mock_writer, NotifyUpdatesFaviconInBackground()); |
| observer()->DidUpdateFaviconURL({}); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| EXPECT_CALL(*mock_writer, NotifyUpdatesTitleInBackground()); |
| observer()->TitleWasSet(nullptr); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Brievly switch the tab to foreground to reset the last backgrounded time. |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kForeground)); |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)); |
| web_contents()->WasShown(); |
| web_contents()->WasHidden(); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Audio usage events should be ignored during the post-background grace |
| // period. |
| observer()->OnAudioStateChanged(true); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| test_clock().Advance(params.audio_usage_grace_period); |
| EXPECT_CALL(*mock_writer, NotifyUsesAudioInBackground()); |
| observer()->OnAudioStateChanged(true); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| FeatureEventsIgnoredWhenLoadingInBackground) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| |
| // Send dummy events to simulate the initial title/favicon update (as these |
| // are ignored). |
| observer()->DidUpdateFaviconURL({}); |
| observer()->TitleWasSet(nullptr); |
| |
| TabLoadTracker::Get()->TransitionStateForTesting(web_contents(), |
| LoadingState::LOADING); |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)); |
| web_contents()->WasHidden(); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| observer()->DidUpdateFaviconURL({}); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| observer()->TitleWasSet(nullptr); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| observer()->OnAudioStateChanged(true); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| NotificationEventsWhenLoadingInBackground) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| |
| TabLoadTracker::Get()->TransitionStateForTesting(web_contents(), |
| LoadingState::LOADING); |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)); |
| web_contents()->WasHidden(); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, NotifyUsesNotificationsInBackground()); |
| observer()->OnNonPersistentNotificationCreated(web_contents(), |
| GetNavIdForWebContents()); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, VisibilityEvent) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| |
| // Test that the visibility events get forwarded to the writer. |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)) |
| .Times(2); |
| observer()->OnVisibilityChanged(content::Visibility::OCCLUDED); |
| observer()->OnVisibilityChanged(content::Visibility::HIDDEN); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kForeground)); |
| observer()->OnVisibilityChanged(content::Visibility::VISIBLE); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, LoadEvent) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| |
| // Test that the load/unload events get forwarded to the writer. |
| |
| EXPECT_CALL(*mock_writer, NotifySiteLoaded()); |
| observer()->OnLoadingStateChange(web_contents(), |
| TabLoadTracker::LoadingState::LOADING, |
| TabLoadTracker::LoadingState::LOADED); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, NotifySiteUnloaded()); |
| observer()->OnLoadingStateChange(web_contents(), |
| TabLoadTracker::LoadingState::LOADED, |
| TabLoadTracker::LoadingState::LOADING); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| observer()->OnLoadingStateChange(web_contents(), |
| TabLoadTracker::LoadingState::LOADING, |
| TabLoadTracker::LoadingState::UNLOADED); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Ensure that a transition from UNLOADED to LOADING doesn't cause any call to |
| // NotifySiteUnloaded. |
| observer()->OnLoadingStateChange(web_contents(), |
| TabLoadTracker::LoadingState::LOADING, |
| TabLoadTracker::LoadingState::UNLOADED); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| LateNotificationUsageSignalIsIgnored) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| EXPECT_CALL(*mock_writer, NotifySiteLoaded()); |
| TabLoadTracker::Get()->TransitionStateForTesting(web_contents(), |
| LoadingState::LOADED); |
| |
| EXPECT_CALL(*mock_writer, |
| NotifySiteVisibilityChanged(TabVisibility::kBackground)); |
| web_contents()->WasHidden(); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| auto nav_id = GetNavIdForWebContents(); |
| EXPECT_CALL(*mock_writer, NotifyUsesNotificationsInBackground()); |
| observer()->OnNonPersistentNotificationCreated(web_contents(), nav_id); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Invalidate the navigation ID but keep the same origin, the notification |
| // should get forwarded to the writer. |
| nav_id.navigation_id++; |
| nav_id.url = web_contents()->GetLastCommittedURL().spec(); |
| EXPECT_CALL(*mock_writer, NotifyUsesNotificationsInBackground()); |
| observer()->OnNonPersistentNotificationCreated(web_contents(), nav_id); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Make the URL of the navigation ID point to a different origin, the writer |
| // shouldn't get notified about this event. |
| nav_id.url = "https://not-the-same-url.com"; |
| observer()->OnNonPersistentNotificationCreated(web_contents(), nav_id); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| TEST_F(LocalSiteCharacteristicsWebContentsObserverTest, |
| OnLoadTimePerformanceMeasurement) { |
| MockDataWriter* mock_writer = NavigateAndReturnMockWriter(kTestUrl1); |
| EXPECT_CALL(*mock_writer, NotifySiteLoaded()); |
| TabLoadTracker::Get()->TransitionStateForTesting(web_contents(), |
| LoadingState::LOADED); |
| |
| constexpr base::TimeDelta kExpectedLoadDuration = |
| base::TimeDelta::FromMicroseconds(501); |
| constexpr base::TimeDelta kExpectedCPUTime = |
| base::TimeDelta::FromMicroseconds(1003); |
| constexpr uint64_t kExpectedMemory = 123u; |
| auto nav_id = GetNavIdForWebContents(); |
| EXPECT_CALL(*mock_writer, |
| NotifyLoadTimePerformanceMeasurement( |
| kExpectedLoadDuration, kExpectedCPUTime, kExpectedMemory)); |
| observer()->OnLoadTimePerformanceEstimate(web_contents(), nav_id, |
| kExpectedLoadDuration, |
| kExpectedCPUTime, kExpectedMemory); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| // Verify that a late notification is not persisted (for now). |
| // TODO(siggi): Fix late notifications such that they persist to another |
| // writer. |
| nav_id.navigation_id++; |
| observer()->OnLoadTimePerformanceEstimate(web_contents(), nav_id, |
| kExpectedLoadDuration, |
| kExpectedCPUTime, kExpectedMemory); |
| ::testing::Mock::VerifyAndClear(mock_writer); |
| |
| EXPECT_CALL(*mock_writer, OnDestroy()); |
| } |
| |
| } // namespace resource_coordinator |