blob: 4253db3f7a5fb85429ddbf86ab1136481b8da2ef [file] [log] [blame]
// 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/execution_engine.h"
#include <optional>
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/actor_test_util.h"
#include "chrome/browser/actor/ui/event_dispatcher.h"
#include "chrome/browser/actor/ui/mock_event_dispatcher.h"
#include "chrome/common/actor.mojom.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_render_frame.mojom.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/tabs/public/mock_tab_interface.h"
#include "content/public/test/navigation_simulator.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/mojom/window_features/window_features.mojom.h"
#include "url/gurl.h"
namespace actor {
using ::optimization_guide::proto::BrowserAction;
using testing::_;
using testing::Invoke;
namespace {
constexpr int kFakeContentNodeId = 123;
constexpr char kActionResultHistogram[] =
"Actor.ExecutionEngine.Action.ResultCode";
class FakeChromeRenderFrame : public chrome::mojom::ChromeRenderFrame {
public:
FakeChromeRenderFrame() = default;
~FakeChromeRenderFrame() override = default;
void OverrideBinder(content::RenderFrameHost* rfh) {
blink::AssociatedInterfaceProvider* remote_interfaces =
rfh->GetRemoteAssociatedInterfaces();
remote_interfaces->OverrideBinderForTesting(
chrome::mojom::ChromeRenderFrame::Name_,
base::BindRepeating(&FakeChromeRenderFrame::Bind,
base::Unretained(this)));
}
// chrome::mojom::ChromeRenderFrame:
void SetWindowFeatures(
blink::mojom::WindowFeaturesPtr window_features) override {}
void RequestReloadImageForContextNode() override {}
void RequestBitmapForContextNode(
RequestBitmapForContextNodeCallback callback) override {}
void RequestBitmapForContextNodeWithBoundsHint(
RequestBitmapForContextNodeWithBoundsHintCallback callback) override {}
void RequestBoundsHintForAllImages(
RequestBoundsHintForAllImagesCallback callback) override {}
void RequestImageForContextNode(
int32_t image_min_area_pixels,
const gfx::Size& image_max_size_pixels,
chrome::mojom::ImageFormat image_format,
int32_t quality,
RequestImageForContextNodeCallback callback) override {}
void ExecuteWebUIJavaScript(const std::u16string& javascript) override {}
void GetMediaFeedURL(GetMediaFeedURLCallback callback) override {}
void LoadBlockedPlugins(const std::string& identifier) override {}
void SetSupportsDraggableRegions(bool supports_draggable_regions) override {}
void SetShouldDeferMediaLoad(bool should_defer) override {}
void InvokeTool(actor::mojom::ToolInvocationPtr request,
InvokeToolCallback callback) override {
std::move(callback).Run(MakeOkResult());
}
void StartActorJournal(
mojo::PendingAssociatedRemote<actor::mojom::JournalClient> client)
override {}
private:
void Bind(mojo::ScopedInterfaceEndpointHandle handle) {
receivers_.Add(
this, mojo::PendingAssociatedReceiver<chrome::mojom::ChromeRenderFrame>(
std::move(handle)));
}
mojo::AssociatedReceiverSet<chrome::mojom::ChromeRenderFrame> receivers_;
};
class ExecutionEngineTest : public ChromeRenderViewHostTestHarness {
public:
ExecutionEngineTest() = default;
~ExecutionEngineTest() override = default;
void SetUp() override {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/{features::kGlicActor},
/*disabled_features=*/{});
ChromeRenderViewHostTestHarness::SetUp();
AssociateTabInterface();
std::unique_ptr<ui::UiEventDispatcher> ui_event_dispatcher =
ui::NewMockUiEventDispatcher();
mock_ui_event_dispatcher_ =
static_cast<ui::MockUiEventDispatcher*>(ui_event_dispatcher.get());
auto execution_engine = ExecutionEngine::CreateForTesting(
profile(), std::move(ui_event_dispatcher), GetTab());
task_ = std::make_unique<ActorTask>(std::move(execution_engine));
}
void TearDown() override {
mock_ui_event_dispatcher_ = nullptr;
task_.reset();
ClearTabInterface();
ChromeRenderViewHostTestHarness::TearDown();
}
protected:
// Note: action must be generated from a callback because this method
// navigates the render frame and the generated action must include a document
// identifier token which is only available after the navigation.
bool Act(const GURL& url, base::OnceCallback<BrowserAction()> make_action) {
content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
url);
fake_chrome_render_frame_.OverrideBinder(main_rfh());
base::test::TestFuture<mojom::ActionResultPtr> success;
BrowserAction action = std::move(make_action).Run();
task_->GetExecutionEngine()->Act(action, success.GetCallback());
return IsOk(*success.Get());
}
tabs::MockTabInterface* GetTab() {
return tab_state_ ? &tab_state_->tab : nullptr;
}
void AssociateTabInterface() { tab_state_.emplace(web_contents()); }
void ClearTabInterface() { tab_state_.reset(); }
auto UiEventDispatcherCallback(mojom::ActionResultPtr result) {
return [result = std::move(result)](
Profile*, const ToolRequest&,
ui::UiEventDispatcher::UiCompleteCallback callback) mutable {
std::move(callback).Run(std::move(result));
};
}
base::HistogramTester histograms_;
FakeChromeRenderFrame fake_chrome_render_frame_;
std::unique_ptr<ActorTask> task_;
raw_ptr<ui::MockUiEventDispatcher> mock_ui_event_dispatcher_;
private:
struct TabState {
explicit TabState(content::WebContents* web_contents) {
ON_CALL(tab, GetContents).WillByDefault(::testing::Return(web_contents));
ON_CALL(tab, RegisterWillDetach)
.WillByDefault([this](tabs::TabInterface::WillDetach callback) {
return will_detach_callback_list_.Add(std::move(callback));
});
}
~TabState() {
will_detach_callback_list_.Notify(
&tab, tabs::TabInterface::DetachReason::kDelete);
}
using WillDetachCallbackList =
base::RepeatingCallbackList<void(tabs::TabInterface*,
tabs::TabInterface::DetachReason)>;
WillDetachCallbackList will_detach_callback_list_;
tabs::MockTabInterface tab;
};
std::optional<TabState> tab_state_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(ExecutionEngineTest, ActSucceedsOnSupportedUrl) {
EXPECT_CALL(*mock_ui_event_dispatcher_, OnPreTool(_, _, _))
.WillOnce(Invoke(UiEventDispatcherCallback(MakeOkResult())));
EXPECT_CALL(*mock_ui_event_dispatcher_, OnPostTool(_, _, _))
.WillOnce(Invoke(UiEventDispatcherCallback(MakeOkResult())));
EXPECT_TRUE(
Act(GURL("http://localhost/"), base::BindLambdaForTesting([this]() {
return MakeClick(*main_rfh(), kFakeContentNodeId);
})));
histograms_.ExpectUniqueSample(kActionResultHistogram,
mojom::ActionResultCode::kOk, 1);
}
TEST_F(ExecutionEngineTest, ActFailsOnUnsupportedUrl) {
EXPECT_CALL(*mock_ui_event_dispatcher_, OnPreTool(_, _, _)).Times(0);
EXPECT_CALL(*mock_ui_event_dispatcher_, OnPostTool(_, _, _)).Times(0);
EXPECT_FALSE(Act(GURL(chrome::kChromeUIVersionURL),
base::BindLambdaForTesting([this]() {
return MakeClick(*main_rfh(), kFakeContentNodeId);
})));
}
// TODO(crbug.com/425784083): Add testing that covers errors returned from
// UiEventDispatcher.
TEST_F(ExecutionEngineTest, ActFailsWhenTabDestroyed) {
content::NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents(), GURL("http://localhost/"));
base::test::TestFuture<mojom::ActionResultPtr> result;
auto execution_engine =
std::make_unique<ExecutionEngine>(profile(), GetTab());
ActorTask task(std::move(execution_engine));
FakeChromeRenderFrame fake_chrome_render_frame;
fake_chrome_render_frame.OverrideBinder(main_rfh());
task.GetExecutionEngine()->Act(MakeClick(*main_rfh(), kFakeContentNodeId),
result.GetCallback());
ClearTabInterface();
DeleteContents();
ExpectErrorResult(result, mojom::ActionResultCode::kTabWentAway);
histograms_.ExpectUniqueSample(kActionResultHistogram,
mojom::ActionResultCode::kTabWentAway, 1);
}
TEST_F(ExecutionEngineTest, CrossOriginNavigationBeforeAction) {
content::NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents(), GURL("http://localhost/"));
FakeChromeRenderFrame fake_chrome_render_frame;
fake_chrome_render_frame.OverrideBinder(main_rfh());
base::test::TestFuture<mojom::ActionResultPtr> result;
auto execution_engine =
std::make_unique<ExecutionEngine>(profile(), GetTab());
ActorTask task(std::move(execution_engine));
task.GetExecutionEngine()->Act(MakeClick(*main_rfh(), kFakeContentNodeId),
result.GetCallback());
// Before the action happens, commit a cross-origin navigation.
ASSERT_FALSE(result.IsReady());
content::NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents(), GURL("http://localhost:8000/"));
// TODO(mcnee): We currently just fail, but this should do something more
// graceful.
ExpectErrorResult(result, mojom::ActionResultCode::kCrossOriginNavigation);
histograms_.ExpectUniqueSample(
kActionResultHistogram, mojom::ActionResultCode::kCrossOriginNavigation,
1);
}
} // namespace
} // namespace actor