blob: ab3c5ba1aa6fdee7f675fe24ce31194a799354ab [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/tools/observation_delay_controller.h"
#include <string>
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/with_feature_override.h"
#include "base/time/time.h"
#include "chrome/browser/actor/actor_features.h"
#include "chrome/browser/actor/tools/observation_delay_test_util.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/common/actor/task_id.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/content/browser/test_autofill_client_injector.h"
#include "components/autofill/content/browser/test_content_autofill_client.h"
#include "components/autofill/core/browser/form_predictions_tracker.h"
#include "components/autofill/core/browser/form_predictions_tracker_test_api.h"
#include "components/autofill/core/browser/mock_form_predictions_tracker.h"
#include "components/autofill/core/common/form_data.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "url/gurl.h"
namespace actor {
namespace {
using ::base::test::ScopedFeatureList;
using ::base::test::TestFuture;
using ::content::BeginNavigateToURLFromRenderer;
using ::content::RenderFrameHost;
using ::content::TestNavigationManager;
using ::content::WebContents;
using ::tabs::TabInterface;
using State = ::actor::ObservationDelayController::State;
class ObservationDelayControllerTest : public ObservationDelayTest {
public:
ObservationDelayControllerTest() {
scoped_feature_list_.InitAndEnableFeatureWithParameters(
features::kGlicActor,
{// Effectively disable the timeout to prevent flakes.
{features::kGlicActorPageStabilityTimeout.name, "30000ms"},
// Use small LCP delay.
{features::kActorObservationDelayLcp.name, "100ms"}});
}
~ObservationDelayControllerTest() override = default;
private:
ScopedFeatureList scoped_feature_list_;
};
class ObservationDelayControllerNavigateTest
: public ObservationDelayControllerTest,
public base::test::WithFeatureOverride {
public:
ObservationDelayControllerNavigateTest()
: base::test::WithFeatureOverride(
kActorRestartObservationDelayControllerOnNavigate) {}
};
IN_PROC_BROWSER_TEST_P(ObservationDelayControllerNavigateTest,
NavigateDuringPageStabilization) {
ASSERT_TRUE(
content::NavigateToURL(web_contents(), GetPageStabilityTestURL()));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
// Initiate a fetch to block page stability.
ASSERT_TRUE(InitiateFetchRequest());
// Start waiting on the controller. It should be blocked in page stability.
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
const GURL url = embedded_test_server()->GetURL("/actor/blank.html");
TestNavigationManager manager(web_contents(), url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents(), url));
if (IsParamFeatureEnabled()) {
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_EQ(result.Get(), ObservationDelayController::Result::kPageNavigated);
} else {
// Stop before committing the navigation. The observer should remain waiting
// on page stability.
ASSERT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
// Complete the navigation. The controller should wait for load, then a
// visual update, then complete.
ASSERT_TRUE(manager.WaitForNavigationFinished());
ASSERT_TRUE(controller.WaitForState(State::kWaitForLoadCompletion));
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_EQ(result.Get(), ObservationDelayController::Result::kOk);
}
}
IN_PROC_BROWSER_TEST_P(ObservationDelayControllerNavigateTest,
NavigateWithTooManyRestarts) {
ASSERT_TRUE(
content::NavigateToURL(web_contents(), GetPageStabilityTestURL()));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
// Force the navigation count to be very large.
controller.SetNavigationCount(1000);
// Initiate a fetch to block page stability.
ASSERT_TRUE(InitiateFetchRequest());
// Start waiting on the controller. It should be blocked in page stability.
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
const GURL url = embedded_test_server()->GetURL("/actor/blank.html");
TestNavigationManager manager(web_contents(), url);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(web_contents(), url));
// Stop before committing the navigation. The observer should remain waiting
// on page stability.
ASSERT_TRUE(manager.WaitForResponse());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
// Complete the navigation. The controller should wait for load, then a
// visual update, then complete.
ASSERT_TRUE(manager.WaitForNavigationFinished());
ASSERT_TRUE(controller.WaitForState(State::kWaitForLoadCompletion));
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_EQ(result.Get(), ObservationDelayController::Result::kOk);
}
IN_PROC_BROWSER_TEST_F(ObservationDelayControllerTest,
UsePageStabilityForSameDocumentNavigation) {
ASSERT_TRUE(
content::NavigateToURL(web_contents(), GetPageStabilityTestURL()));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
// Perform a same-document navigation. The page has a navigation handler
// that will initiate a fetch from this event.
ASSERT_TRUE(InitiateFetchRequest());
// Start waiting on the controller. It should be blocked in page stability.
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
EXPECT_FALSE(result.IsReady());
Respond("TEST COMPLETE");
ASSERT_TRUE(controller.WaitForState(State::kWaitForLoadCompletion));
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(result.Wait());
ASSERT_EQ(GetOutputText(), "TEST COMPLETE");
}
// Test waiting on a new document load after waiting for the page to stabilize.
IN_PROC_BROWSER_TEST_P(ObservationDelayControllerNavigateTest,
LoadAfterStability) {
ASSERT_TRUE(
content::NavigateToURL(web_contents(), GetPageStabilityTestURL()));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
ASSERT_TRUE(InitiateFetchRequest());
// Start waiting, since a fetch is in progress we should be waiting for page
// stability.
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
EXPECT_FALSE(result.IsReady());
// Start a navigation to a page that finishes navigating but is deferred on
// the load event.
NavigateToLoadDeferredPage deferred_navigation(web_contents(),
embedded_test_server());
ASSERT_TRUE(deferred_navigation.RunToDOMContentLoadedEvent());
if (IsParamFeatureEnabled()) {
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_EQ(result.Get(), ObservationDelayController::Result::kPageNavigated);
} else {
// The controller should reach the loading state and stay there.
ASSERT_TRUE(
DoesReachSteadyState(controller, State::kWaitForLoadCompletion));
EXPECT_FALSE(result.IsReady());
// Unblock the subframe, the controller should now proceed through the
// remaining states.
ASSERT_TRUE(deferred_navigation.RunToLoadEvent());
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_EQ(result.Get(), ObservationDelayController::Result::kOk);
}
}
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(ObservationDelayControllerNavigateTest);
// Ensure that putting a tab into the background while its waiting to stabilize
// doesn't affect the PageStabilityMonitor.
// TODO(b/448641423): This test better belongs in PageStabilityMonitor browser
// tests but is much clearer to write here. Move once the tests are sharing
// infrastructure.
IN_PROC_BROWSER_TEST_F(ObservationDelayControllerTest,
BackgroundTabWhileWaitingForStability) {
ASSERT_TRUE(
content::NavigateToURL(web_contents(), GetPageStabilityTestURL()));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
ASSERT_TRUE(InitiateFetchRequest());
// Start waiting, since a fetch is in progress we should be waiting for page
// stability.
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
EXPECT_FALSE(result.IsReady());
// Ensure the tab can still produce frames while backgrounded.
auto scoped_decrement_closure =
web_contents()->IncrementCapturerCount(gfx::Size(),
/*stay_hidden=*/false,
/*stay_awake=*/true,
/*is_activity=*/true);
TabInterface* observed_tab = active_tab();
ASSERT_TRUE(observed_tab->IsActivated());
// Now open a new tab, putting the tab waiting on page stability in the
// background.
ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("about:blank"), WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
ASSERT_FALSE(observed_tab->IsActivated());
ASSERT_NE(active_tab(), observed_tab);
// Ensure the controller doesn't break out of waiting for page stability.
EXPECT_TRUE(DoesReachSteadyState(controller, State::kWaitForPageStability));
}
class ObservationDelayControllerLcpTest : public ObservationDelayTest {
public:
static constexpr int kLcpDelayInMs = 3000;
ObservationDelayControllerLcpTest() {
std::string lcp_delay = absl::StrFormat("%dms", kLcpDelayInMs);
scoped_feature_list_.InitAndEnableFeatureWithParameters(
features::kGlicActor,
{// Effectively disable the timeout to prevent flakes.
{features::kGlicActorPageStabilityTimeout.name, "30000ms"},
// Do not use min wait
{features::kGlicActorPageStabilityMinWait.name, "0ms"},
{features::kActorObservationDelayLcp.name, lcp_delay}});
}
~ObservationDelayControllerLcpTest() override = default;
private:
ScopedFeatureList scoped_feature_list_;
};
// Tests that no delay is applied when LCP is already available.
IN_PROC_BROWSER_TEST_F(ObservationDelayControllerLcpTest, NoDelayWhenLcpReady) {
const GURL url = embedded_test_server()->GetURL("/title1.html");
auto waiter = std::make_unique<page_load_metrics::PageLoadMetricsTestWaiter>(
web_contents());
waiter->AddPageExpectation(page_load_metrics::PageLoadMetricsTestWaiter::
TimingField::kLargestContentfulPaint);
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Wait for the LCP metric to be fully reported to the browser process.
waiter->Wait();
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
base::ElapsedTimer timer;
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(result.Wait());
// Since the page had a paint, LCP is considered valid, and we should not
// have applied the delay.
EXPECT_LT(timer.Elapsed(), base::Milliseconds(kLcpDelayInMs));
}
// Tests that the LCP delay is correctly applied when a standard page is loaded
// that has no content to paint (and thus no LCP).
IN_PROC_BROWSER_TEST_F(ObservationDelayControllerLcpTest,
DelayIsAppliedForPageWithNoContent) {
// Navigate to an empty html page. This is a standard navigation, so the
// PageLoadMetrics system will run, but no LCP will ever be recorded
// because there is no content.
const GURL url = embedded_test_server()->GetURL("/actor/blank.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
base::ElapsedTimer timer;
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDelayForLcp));
ASSERT_TRUE(result.Wait());
// The total time should be at least the LCP delay, because the empty page
// is tracked but has no contentful paint.
EXPECT_GE(timer.Elapsed(), base::Milliseconds(kLcpDelayInMs));
}
class ObservationDelayControllerExcludeAdRequestsTest
: public ObservationDelayControllerTest,
public base::test::WithFeatureOverride {
public:
ObservationDelayControllerExcludeAdRequestsTest()
: base::test::WithFeatureOverride(
features::kGlicActorObservationDelayExcludeAdFrameLoading) {}
~ObservationDelayControllerExcludeAdRequestsTest() override = default;
};
IN_PROC_BROWSER_TEST_P(ObservationDelayControllerExcludeAdRequestsTest,
ExcludeAdIframeLoad) {
const GURL url = embedded_test_server()->GetURL("/actor/blank.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
// Append an iframe.
EXPECT_TRUE(content::ExecJs(main_frame(), R"(
const frame = document.createElement('iframe');
frame.id = 'child'
document.body.appendChild(frame);
)"));
RenderFrameHost* iframe_rfh = content::ChildFrameAt(main_frame(), 0);
ASSERT_TRUE(iframe_rfh);
// Mark the iframe as an ad frame.
iframe_rfh->UpdateIsAdFrame(/*is_ad_frame=*/true);
const GURL iframe_url = embedded_test_server()->GetURL("/actor/simple.html");
TestNavigationManager iframe_manager(web_contents(), iframe_url);
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
// Initiate iframe navigation.
ASSERT_TRUE(BeginNavigateIframeToURL(web_contents(), /*iframe_id=*/"child",
iframe_url));
ASSERT_TRUE(iframe_manager.WaitForRequestStart());
// The frame tree is loading because the iframe is loading. However, it is
// considered as not loading when excluding ad frames.
ASSERT_TRUE(web_contents()->IsLoading());
ASSERT_FALSE(web_contents()->IsLoadingExcludingAdSubframes());
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
// Regardless of the feature status, the controller should move to wait for
// load completion state after the wait starts.
ASSERT_TRUE(controller.WaitForState(State::kWaitForLoadCompletion));
if (IsParamFeatureEnabled()) {
// The controller immediately advances to the next state as it excludes ad
// frames when waiting for load completion.
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDone));
} else {
// The controller should stay in the waiting for load completion state until
// the iframe navigation finishes.
ASSERT_TRUE(
DoesReachSteadyState(controller, State::kWaitForLoadCompletion));
// Complete the navigation. The controller should wait for visual update,
// then complete.
ASSERT_TRUE(iframe_manager.WaitForNavigationFinished());
ASSERT_TRUE(controller.WaitForState(State::kWaitForVisualStateUpdate));
ASSERT_TRUE(controller.WaitForState(State::kMaybeDelayForLcp));
ASSERT_TRUE(controller.WaitForState(State::kDone));
}
ASSERT_TRUE(result.Wait());
}
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
ObservationDelayControllerExcludeAdRequestsTest);
class ObservationAutofillClient : public autofill::TestContentAutofillClient {
public:
explicit ObservationAutofillClient(content::WebContents* web_contents)
: autofill::TestContentAutofillClient(web_contents) {
auto tracker = std::make_unique<
testing::StrictMock<autofill::MockFormPredictionsTracker>>(this);
mock_tracker_ptr_ = tracker.get();
set_form_predictions_tracker(std::move(tracker));
}
autofill::MockFormPredictionsTracker& mock_tracker() {
return *mock_tracker_ptr_;
}
private:
// Owned by the base class.
raw_ptr<autofill::MockFormPredictionsTracker> mock_tracker_ptr_ = nullptr;
};
class ObservationDelayControllerAutofillTest
: public ObservationDelayControllerTest,
public testing::WithParamInterface<int> {
public:
static constexpr int kAutofillParsingTimeoutInMs = 3000;
ObservationDelayControllerAutofillTest() {
std::string autofill_parsing_timeout =
absl::StrFormat("%dms", kAutofillParsingTimeoutInMs);
int lcp_delay_in_ms = GetParam();
std::string lcp_delay = absl::StrFormat("%dms", lcp_delay_in_ms);
feature_list_.InitWithFeaturesAndParameters(
{{autofill::features::kAutofillDelayApcForPredictions, {}},
{features::kGlicActor,
{// Effectively disable stability timeout to prevent flakes.
{features::kGlicActorPageStabilityTimeout.name, "30000ms"},
// Do not use min wait for stability so that it happens immediately.
{features::kGlicActorPageStabilityMinWait.name, "0ms"},
// wait for LCP quickly so that it happens immediately.
{features::kActorObservationDelayLcp.name, lcp_delay},
{features::kActorObservationDelayAutofillPredictionsTimeout.name,
autofill_parsing_timeout},
// Timeout the overall process after 15 seconds.
{features::kActorObservationDelayTimeout.name, "15000ms"}}}},
{});
}
ObservationAutofillClient* autofill_client() {
return autofill_client_injector_[web_contents()];
}
private:
base::test::ScopedFeatureList feature_list_;
autofill::TestAutofillClientInjector<ObservationAutofillClient>
autofill_client_injector_;
};
// Tests that if there is a `FormPredictionsTracker`, state is moved to
// `kWaitForAutofillPredictions` and then to `kDone` once the tracker completes.
IN_PROC_BROWSER_TEST_P(ObservationDelayControllerAutofillTest,
FormPredictionsTrackerCallsBack) {
autofill::MockFormPredictionsTracker& tracker =
autofill_client()->mock_tracker();
EXPECT_CALL(tracker, Wait)
.WillOnce(testing::WithArgs<0, 1>(
[](base::OnceClosure callback, base::TimeDelta timeout) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, std::move(callback), timeout);
}));
const GURL url = embedded_test_server()->GetURL("/actor/blank.html");
ASSERT_TRUE(content::NavigateToURL(web_contents(), url));
TestObservationDelayController controller(*main_frame(), actor::TaskId(),
journal(), PageStabilityConfig());
base::ElapsedTimer timer;
TestFuture<ObservationDelayController::Result> result;
controller.Wait(*active_tab(), result.GetCallback());
ASSERT_TRUE(controller.WaitForState(State::kWaitForAutofillPredictions));
// The state machine advances to done once the callback from the tracker is
// executed.
ASSERT_TRUE(controller.WaitForState(State::kDone));
ASSERT_TRUE(result.Wait());
EXPECT_EQ(result.Get(), ObservationDelayController::Result::kOk);
EXPECT_GE(timer.Elapsed(), base::Milliseconds(kAutofillParsingTimeoutInMs));
}
INSTANTIATE_TEST_SUITE_P(All,
ObservationDelayControllerAutofillTest,
testing::Values(0, 50),
[](const testing::TestParamInfo<int>& info) {
return info.param == 0
? "NoLcpDelay"
: base::StringPrintf("LcpDelay_%dms",
info.param);
});
} // namespace
} // namespace actor